From 5082ff3252f60f2ed6f4ed3789a1f142e1667e1f Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 29 Apr 2020 18:43:15 +0200 Subject: [PATCH 001/122] use cache busting for KP bundles (#64414) * convert into TS * load plugin scripts in html body * use buildNum as a unique Id for cache busting * add tests for caching * fix tests * remove the last TODO. url should be inlined with assetss server * this logic handled by publicPathMap on the client * cache kbn-shared-deps as well * attempt to fix karma tests * always run file through replace stream * place buildHash at begining of path, include all static files * update bundles_route tests to inject buildNum everywhere * fix karma config to point to right prefix * use isDist naming throughout * explain magic number with variables * restore replacePublicPath option from #64226 * replace one more instance of replacePublicPath * use promisify instead of bluebird + non-null assertions * remove one more magic number Co-authored-by: spalger Co-authored-by: Elastic Machine --- src/legacy/ui/ui_render/ui_render_mixin.js | 15 ++- .../bundles_route/__tests__/bundles_route.js | 119 +++++++++++++++--- src/optimize/bundles_route/bundles_route.ts | 35 ++++-- .../bundles_route/dynamic_asset_response.ts | 25 +++- .../bundles_route/proxy_bundles_route.ts | 16 ++- src/optimize/np_ui_plugin_public_dirs.ts | 1 - src/optimize/optimize_mixin.ts | 2 + src/optimize/watch/optmzr_role.js | 3 +- src/optimize/watch/proxy_role.js | 1 + src/optimize/watch/watch_optimizer.js | 3 +- src/optimize/watch/watch_server.js | 10 +- tasks/config/karma.js | 26 ++-- 12 files changed, 195 insertions(+), 61 deletions(-) diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index d42d6d556b37b..692b787ccf405 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -96,9 +96,12 @@ export function uiRenderMixin(kbnServer, server, config) { ? await uiSettings.get('theme:darkMode') : false; + const buildHash = server.newPlatform.env.packageInfo.buildNum; const basePath = config.get('server.basePath'); - const regularBundlePath = `${basePath}/bundles`; - const dllBundlePath = `${basePath}/built_assets/dlls`; + + const regularBundlePath = `${basePath}/${buildHash}/bundles`; + const dllBundlePath = `${basePath}/${buildHash}/built_assets/dlls`; + const dllStyleChunks = DllCompiler.getRawDllConfig().chunks.map( chunk => `${dllBundlePath}/vendors${chunk}.style.dll.css` ); @@ -108,15 +111,15 @@ export function uiRenderMixin(kbnServer, server, config) { const styleSheetPaths = [ ...(isCore ? [] : dllStyleChunks), - `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, ...(darkMode ? [ - `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`, + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`, `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, `${regularBundlePath}/dark_theme.style.css`, ] : [ - `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, `${regularBundlePath}/light_theme.style.css`, ]), @@ -131,7 +134,7 @@ export function uiRenderMixin(kbnServer, server, config) { ) .map(path => path.localPath.endsWith('.scss') - ? `${basePath}/built_assets/css/${path.publicPath}` + ? `${basePath}/${buildHash}/built_assets/css/${path.publicPath}` : `${basePath}/${path.publicPath}` ) .reverse(), diff --git a/src/optimize/bundles_route/__tests__/bundles_route.js b/src/optimize/bundles_route/__tests__/bundles_route.js index 0b2aeda11fb0e..902fa59b20569 100644 --- a/src/optimize/bundles_route/__tests__/bundles_route.js +++ b/src/optimize/bundles_route/__tests__/bundles_route.js @@ -32,6 +32,7 @@ import { PUBLIC_PATH_PLACEHOLDER } from '../../public_path_placeholder'; const chance = new Chance(); const outputFixture = resolve(__dirname, './fixtures/output'); +const pluginNoPlaceholderFixture = resolve(__dirname, './fixtures/plugin/no_placeholder'); const randomWordsCache = new Set(); const uniqueRandomWord = () => { @@ -58,6 +59,9 @@ describe('optimizer/bundle route', () => { dllBundlesPath = outputFixture, basePublicPath = '', builtCssPath = outputFixture, + npUiPluginPublicDirs = [], + buildHash = '1234', + isDist = false, } = options; const server = new Hapi.Server(); @@ -69,6 +73,9 @@ describe('optimizer/bundle route', () => { dllBundlesPath, basePublicPath, builtCssPath, + npUiPluginPublicDirs, + buildHash, + isDist, }) ); @@ -158,7 +165,7 @@ describe('optimizer/bundle route', () => { it('responds with exact file data', async () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/image.png', + url: '/1234/bundles/image.png', }); expect(response.statusCode).to.be(200); @@ -173,7 +180,7 @@ describe('optimizer/bundle route', () => { it('responds with no content-length and exact file data', async () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }); expect(response.statusCode).to.be(200); @@ -187,12 +194,12 @@ describe('optimizer/bundle route', () => { }); describe('js file with placeholder', () => { - it('responds with no content-length and modified file data', async () => { + it('responds with no content-length and modifiedfile data ', async () => { const basePublicPath = `/${uniqueRandomWord()}`; const server = createServer({ basePublicPath }); const response = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', }); expect(response.statusCode).to.be(200); @@ -204,7 +211,7 @@ describe('optimizer/bundle route', () => { ); expect(response.result.indexOf(source)).to.be(-1); expect(response.result).to.be( - replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/bundles/`) + replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/1234/bundles/`) ); }); }); @@ -213,7 +220,7 @@ describe('optimizer/bundle route', () => { it('responds with no content-length and exact file data', async () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/no_placeholder.css', + url: '/1234/bundles/no_placeholder.css', }); expect(response.statusCode).to.be(200); @@ -231,7 +238,7 @@ describe('optimizer/bundle route', () => { const server = createServer({ basePublicPath }); const response = await server.inject({ - url: '/bundles/with_placeholder.css', + url: '/1234/bundles/with_placeholder.css', }); expect(response.statusCode).to.be(200); @@ -240,7 +247,7 @@ describe('optimizer/bundle route', () => { expect(response.headers).to.have.property('content-type', 'text/css; charset=utf-8'); expect(response.result.indexOf(source)).to.be(-1); expect(response.result).to.be( - replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/bundles/`) + replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/1234/bundles/`) ); }); }); @@ -250,7 +257,7 @@ describe('optimizer/bundle route', () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/../outside_output.js', + url: '/1234/bundles/../outside_output.js', }); expect(response.statusCode).to.be(404); @@ -267,7 +274,7 @@ describe('optimizer/bundle route', () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/non_existent.js', + url: '/1234/bundles/non_existent.js', }); expect(response.statusCode).to.be(404); @@ -286,7 +293,7 @@ describe('optimizer/bundle route', () => { }); const response = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', }); expect(response.statusCode).to.be(404); @@ -306,7 +313,7 @@ describe('optimizer/bundle route', () => { sinon.assert.notCalled(createHash); const resp1 = await server.inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }); sinon.assert.calledOnce(createHash); @@ -314,23 +321,23 @@ describe('optimizer/bundle route', () => { expect(resp1.statusCode).to.be(200); const resp2 = await server.inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }); sinon.assert.notCalled(createHash); expect(resp2.statusCode).to.be(200); }); - it('is unique per basePublicPath although content is the same', async () => { + it('is unique per basePublicPath although content is the same (by default)', async () => { const basePublicPath1 = `/${uniqueRandomWord()}`; const basePublicPath2 = `/${uniqueRandomWord()}`; const [resp1, resp2] = await Promise.all([ createServer({ basePublicPath: basePublicPath1 }).inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }), createServer({ basePublicPath: basePublicPath2 }).inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }), ]); @@ -349,13 +356,13 @@ describe('optimizer/bundle route', () => { it('responds with 304 when etag and last modified are sent back', async () => { const server = createServer(); const resp = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', }); expect(resp.statusCode).to.be(200); const resp2 = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', headers: { 'if-modified-since': resp.headers['last-modified'], 'if-none-match': resp.headers.etag, @@ -366,4 +373,80 @@ describe('optimizer/bundle route', () => { expect(resp2.result).to.have.length(0); }); }); + + describe('kibana platform assets', () => { + describe('caching', () => { + describe('for non-distributable mode', () => { + it('uses "etag" header to invalidate cache', async () => { + const basePublicPath = `/${uniqueRandomWord()}`; + + const npUiPluginPublicDirs = [ + { + id: 'no_placeholder', + path: pluginNoPlaceholderFixture, + }, + ]; + const responce = await createServer({ basePublicPath, npUiPluginPublicDirs }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }); + + expect(responce.statusCode).to.be(200); + + expect(responce.headers.etag).to.be.a('string'); + expect(responce.headers['cache-control']).to.be('must-revalidate'); + }); + + it('creates the same "etag" header for the same content with the same basePath', async () => { + const npUiPluginPublicDirs = [ + { + id: 'no_placeholder', + path: pluginNoPlaceholderFixture, + }, + ]; + const [resp1, resp2] = await Promise.all([ + createServer({ basePublicPath: '', npUiPluginPublicDirs }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }), + createServer({ basePublicPath: '', npUiPluginPublicDirs }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }), + ]); + + expect(resp1.statusCode).to.be(200); + expect(resp2.statusCode).to.be(200); + + expect(resp1.rawPayload).to.eql(resp2.rawPayload); + + expect(resp1.headers.etag).to.be.a('string'); + expect(resp2.headers.etag).to.be.a('string'); + expect(resp1.headers.etag).to.eql(resp2.headers.etag); + }); + }); + + describe('for distributable mode', () => { + it('commands to cache assets for each release for a year', async () => { + const basePublicPath = `/${uniqueRandomWord()}`; + + const npUiPluginPublicDirs = [ + { + id: 'no_placeholder', + path: pluginNoPlaceholderFixture, + }, + ]; + const responce = await createServer({ + basePublicPath, + npUiPluginPublicDirs, + isDist: true, + }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }); + + expect(responce.statusCode).to.be(200); + + expect(responce.headers.etag).to.be(undefined); + expect(responce.headers['cache-control']).to.be('max-age=31536000'); + }); + }); + }); + }); }); diff --git a/src/optimize/bundles_route/bundles_route.ts b/src/optimize/bundles_route/bundles_route.ts index 5605ec5338969..e9cfba0130d95 100644 --- a/src/optimize/bundles_route/bundles_route.ts +++ b/src/optimize/bundles_route/bundles_route.ts @@ -47,12 +47,16 @@ export function createBundlesRoute({ basePublicPath, builtCssPath, npUiPluginPublicDirs = [], + buildHash, + isDist = false, }: { regularBundlesPath: string; dllBundlesPath: string; basePublicPath: string; builtCssPath: string; npUiPluginPublicDirs?: NpUiPluginPublicDirs; + buildHash: string; + isDist?: boolean; }) { // rather than calculate the fileHash on every request, we // provide a cache object to `resolveDynamicAssetResponse()` that @@ -82,45 +86,51 @@ export function createBundlesRoute({ return [ buildRouteForBundles({ - publicPath: `${basePublicPath}/bundles/kbn-ui-shared-deps/`, - routePath: '/bundles/kbn-ui-shared-deps/', + publicPath: `${basePublicPath}/${buildHash}/bundles/kbn-ui-shared-deps/`, + routePath: `/${buildHash}/bundles/kbn-ui-shared-deps/`, bundlesPath: UiSharedDeps.distDir, fileHashCache, replacePublicPath: false, + isDist, }), ...npUiPluginPublicDirs.map(({ id, path }) => buildRouteForBundles({ - publicPath: `${basePublicPath}/bundles/plugin/${id}/`, - routePath: `/bundles/plugin/${id}/`, + publicPath: `${basePublicPath}/${buildHash}/bundles/plugin/${id}/`, + routePath: `/${buildHash}/bundles/plugin/${id}/`, bundlesPath: path, fileHashCache, replacePublicPath: false, + isDist, }) ), buildRouteForBundles({ - publicPath: `${basePublicPath}/bundles/core/`, - routePath: `/bundles/core/`, + publicPath: `${basePublicPath}/${buildHash}/bundles/core/`, + routePath: `/${buildHash}/bundles/core/`, bundlesPath: fromRoot(join('src', 'core', 'target', 'public')), fileHashCache, replacePublicPath: false, + isDist, }), buildRouteForBundles({ - publicPath: `${basePublicPath}/bundles/`, - routePath: '/bundles/', + publicPath: `${basePublicPath}/${buildHash}/bundles/`, + routePath: `/${buildHash}/bundles/`, bundlesPath: regularBundlesPath, fileHashCache, + isDist, }), buildRouteForBundles({ - publicPath: `${basePublicPath}/built_assets/dlls/`, - routePath: '/built_assets/dlls/', + publicPath: `${basePublicPath}/${buildHash}/built_assets/dlls/`, + routePath: `/${buildHash}/built_assets/dlls/`, bundlesPath: dllBundlesPath, fileHashCache, + isDist, }), buildRouteForBundles({ publicPath: `${basePublicPath}/`, - routePath: '/built_assets/css/', + routePath: `/${buildHash}/built_assets/css/`, bundlesPath: builtCssPath, fileHashCache, + isDist, }), ]; } @@ -131,12 +141,14 @@ function buildRouteForBundles({ bundlesPath, fileHashCache, replacePublicPath = true, + isDist, }: { publicPath: string; routePath: string; bundlesPath: string; fileHashCache: FileHashCache; replacePublicPath?: boolean; + isDist: boolean; }) { return { method: 'GET', @@ -159,6 +171,7 @@ function buildRouteForBundles({ fileHashCache, publicPath, replacePublicPath, + isDist, }); }, }, diff --git a/src/optimize/bundles_route/dynamic_asset_response.ts b/src/optimize/bundles_route/dynamic_asset_response.ts index bebc062ee949d..a020c6935eeec 100644 --- a/src/optimize/bundles_route/dynamic_asset_response.ts +++ b/src/optimize/bundles_route/dynamic_asset_response.ts @@ -17,8 +17,8 @@ * under the License. */ -import { resolve } from 'path'; import Fs from 'fs'; +import { resolve } from 'path'; import { promisify } from 'util'; import Boom from 'boom'; @@ -26,8 +26,13 @@ import Hapi from 'hapi'; import { FileHashCache } from './file_hash_cache'; import { getFileHash } from './file_hash'; +// @ts-ignore import { replacePlaceholder } from '../public_path_placeholder'; +const MINUTE = 60; +const HOUR = 60 * MINUTE; +const DAY = 24 * HOUR; + const asyncOpen = promisify(Fs.open); const asyncClose = promisify(Fs.close); const asyncFstat = promisify(Fs.fstat); @@ -58,6 +63,7 @@ export async function createDynamicAssetResponse({ publicPath, fileHashCache, replacePublicPath, + isDist, }: { request: Hapi.Request; h: Hapi.ResponseToolkit; @@ -65,6 +71,7 @@ export async function createDynamicAssetResponse({ publicPath: string; fileHashCache: FileHashCache; replacePublicPath: boolean; + isDist: boolean; }) { let fd: number | undefined; @@ -82,7 +89,7 @@ export async function createDynamicAssetResponse({ fd = await asyncOpen(path, 'r'); const stat = await asyncFstat(fd); - const hash = await getFileHash(fileHashCache, path, stat, fd); + const hash = isDist ? undefined : await getFileHash(fileHashCache, path, stat, fd); const read = Fs.createReadStream(null as any, { fd, @@ -92,15 +99,21 @@ export async function createDynamicAssetResponse({ fd = undefined; // read stream is now responsible for fd const content = replacePublicPath ? replacePlaceholder(read, publicPath) : read; - const etag = replacePublicPath ? `${hash}-${publicPath}` : hash; - return h + const response = h .response(content) .takeover() .code(200) - .etag(etag) - .header('cache-control', 'must-revalidate') .type(request.server.mime.path(path).type); + + if (isDist) { + response.header('cache-control', `max-age=${365 * DAY}`); + } else { + response.etag(`${hash}-${publicPath}`); + response.header('cache-control', 'must-revalidate'); + } + + return response; } catch (error) { if (fd) { try { diff --git a/src/optimize/bundles_route/proxy_bundles_route.ts b/src/optimize/bundles_route/proxy_bundles_route.ts index 97616f7041f1c..1d189054324a1 100644 --- a/src/optimize/bundles_route/proxy_bundles_route.ts +++ b/src/optimize/bundles_route/proxy_bundles_route.ts @@ -17,11 +17,19 @@ * under the License. */ -export function createProxyBundlesRoute({ host, port }: { host: string; port: number }) { +export function createProxyBundlesRoute({ + host, + port, + buildHash, +}: { + host: string; + port: number; + buildHash: string; +}) { return [ - buildProxyRouteForBundles('/bundles/', host, port), - buildProxyRouteForBundles('/built_assets/dlls/', host, port), - buildProxyRouteForBundles('/built_assets/css/', host, port), + buildProxyRouteForBundles(`/${buildHash}/bundles/`, host, port), + buildProxyRouteForBundles(`/${buildHash}/built_assets/dlls/`, host, port), + buildProxyRouteForBundles(`/${buildHash}/built_assets/css/`, host, port), ]; } diff --git a/src/optimize/np_ui_plugin_public_dirs.ts b/src/optimize/np_ui_plugin_public_dirs.ts index 268d9b1485dd8..e7c3207948f6a 100644 --- a/src/optimize/np_ui_plugin_public_dirs.ts +++ b/src/optimize/np_ui_plugin_public_dirs.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import KbnServer from '../legacy/server/kbn_server'; export type NpUiPluginPublicDirs = Array<{ diff --git a/src/optimize/optimize_mixin.ts b/src/optimize/optimize_mixin.ts index 5abe43ca1577a..9a3f08e2f667e 100644 --- a/src/optimize/optimize_mixin.ts +++ b/src/optimize/optimize_mixin.ts @@ -57,6 +57,8 @@ export const optimizeMixin = async ( basePublicPath: config.get('server.basePath'), builtCssPath: fromRoot('built_assets/css'), npUiPluginPublicDirs: getNpUiPluginPublicDirs(kbnServer), + buildHash: kbnServer.newPlatform.env.packageInfo.buildNum.toString(), + isDist: kbnServer.newPlatform.env.packageInfo.dist, }) ); diff --git a/src/optimize/watch/optmzr_role.js b/src/optimize/watch/optmzr_role.js index a31ef7229e5da..1f6107996277c 100644 --- a/src/optimize/watch/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -49,7 +49,8 @@ export default async (kbnServer, kibanaHapiServer, config) => { config.get('optimize.watchPort'), config.get('server.basePath'), watchOptimizer, - getNpUiPluginPublicDirs(kbnServer) + getNpUiPluginPublicDirs(kbnServer), + kbnServer.newPlatform.env.packageInfo.buildNum.toString() ); watchOptimizer.status$.subscribe({ diff --git a/src/optimize/watch/proxy_role.js b/src/optimize/watch/proxy_role.js index 6093658ae1a2d..0f6f3b2d4b622 100644 --- a/src/optimize/watch/proxy_role.js +++ b/src/optimize/watch/proxy_role.js @@ -26,6 +26,7 @@ export default (kbnServer, server, config) => { createProxyBundlesRoute({ host: config.get('optimize.watchHost'), port: config.get('optimize.watchPort'), + buildHash: kbnServer.newPlatform.env.packageInfo.buildNum.toString(), }) ); diff --git a/src/optimize/watch/watch_optimizer.js b/src/optimize/watch/watch_optimizer.js index 6c20f21c7768e..cdff57a00c2e0 100644 --- a/src/optimize/watch/watch_optimizer.js +++ b/src/optimize/watch/watch_optimizer.js @@ -106,7 +106,7 @@ export default class WatchOptimizer extends BaseOptimizer { }); } - bindToServer(server, basePath, npUiPluginPublicDirs) { + bindToServer(server, basePath, npUiPluginPublicDirs, buildHash) { // pause all requests received while the compiler is running // and continue once an outcome is reached (aborting the request // with an error if it was a failure). @@ -118,6 +118,7 @@ export default class WatchOptimizer extends BaseOptimizer { server.route( createBundlesRoute({ npUiPluginPublicDirs: npUiPluginPublicDirs, + buildHash, regularBundlesPath: this.compiler.outputPath, dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, basePublicPath: basePath, diff --git a/src/optimize/watch/watch_server.js b/src/optimize/watch/watch_server.js index 74a96dc8aea6e..81e04a5b83956 100644 --- a/src/optimize/watch/watch_server.js +++ b/src/optimize/watch/watch_server.js @@ -21,10 +21,11 @@ import { Server } from 'hapi'; import { registerHapiPlugins } from '../../legacy/server/http/register_hapi_plugins'; export default class WatchServer { - constructor(host, port, basePath, optimizer, npUiPluginPublicDirs) { + constructor(host, port, basePath, optimizer, npUiPluginPublicDirs, buildHash) { this.basePath = basePath; this.optimizer = optimizer; this.npUiPluginPublicDirs = npUiPluginPublicDirs; + this.buildHash = buildHash; this.server = new Server({ host: host, port: port, @@ -35,7 +36,12 @@ export default class WatchServer { async init() { await this.optimizer.init(); - this.optimizer.bindToServer(this.server, this.basePath, this.npUiPluginPublicDirs); + this.optimizer.bindToServer( + this.server, + this.basePath, + this.npUiPluginPublicDirs, + this.buildHash + ); await this.server.start(); } } diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 1ec7c831b4864..f87edbf04f220 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -25,6 +25,7 @@ import { DllCompiler } from '../../src/optimize/dynamic_dll_plugin'; const TOTAL_CI_SHARDS = 4; const ROOT = dirname(require.resolve('../../package.json')); +const buildHash = String(Number.MAX_SAFE_INTEGER); module.exports = function(grunt) { function pickBrowser() { @@ -57,27 +58,30 @@ module.exports = function(grunt) { 'http://localhost:5610/test_bundle/karma/globals.js', ...UiSharedDeps.jsDepFilenames.map( - chunkFilename => `http://localhost:5610/bundles/kbn-ui-shared-deps/${chunkFilename}` + chunkFilename => + `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${chunkFilename}` ), - `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, + `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, - 'http://localhost:5610/built_assets/dlls/vendors_runtime.bundle.dll.js', + `http://localhost:5610/${buildHash}/built_assets/dlls/vendors_runtime.bundle.dll.js`, ...DllCompiler.getRawDllConfig().chunks.map( - chunk => `http://localhost:5610/built_assets/dlls/vendors${chunk}.bundle.dll.js` + chunk => + `http://localhost:5610/${buildHash}/built_assets/dlls/vendors${chunk}.bundle.dll.js` ), shardNum === undefined - ? `http://localhost:5610/bundles/tests.bundle.js` - : `http://localhost:5610/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${shardNum}`, + ? `http://localhost:5610/${buildHash}/bundles/tests.bundle.js` + : `http://localhost:5610/${buildHash}/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${shardNum}`, - `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, + `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, // this causes tilemap tests to fail, probably because the eui styles haven't been // included in the karma harness a long some time, if ever // `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, ...DllCompiler.getRawDllConfig().chunks.map( - chunk => `http://localhost:5610/built_assets/dlls/vendors${chunk}.style.dll.css` + chunk => + `http://localhost:5610/${buildHash}/built_assets/dlls/vendors${chunk}.style.dll.css` ), - 'http://localhost:5610/bundles/tests.style.css', + `http://localhost:5610/${buildHash}/bundles/tests.style.css`, ]; } @@ -127,9 +131,9 @@ module.exports = function(grunt) { proxies: { '/tests/': 'http://localhost:5610/tests/', - '/bundles/': 'http://localhost:5610/bundles/', - '/built_assets/dlls/': 'http://localhost:5610/built_assets/dlls/', '/test_bundle/': 'http://localhost:5610/test_bundle/', + [`/${buildHash}/bundles/`]: `http://localhost:5610/${buildHash}/bundles/`, + [`/${buildHash}/built_assets/dlls/`]: `http://localhost:5610/${buildHash}/built_assets/dlls/`, }, client: { From 0d4cfba4b44db847600cc6a34cb697deae7675a9 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 29 Apr 2020 09:44:56 -0700 Subject: [PATCH 002/122] share single data plugin bundle (#64549) --- packages/kbn-optimizer/src/worker/webpack.config.ts | 11 +++-------- src/legacy/ui/ui_render/ui_render_mixin.js | 3 ++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 7411e2df1b613..cc3fa8c2720de 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -37,7 +37,7 @@ const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset' const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const STATIC_BUNDLE_PLUGINS = [ - // { id: 'data', dirname: 'data' }, + { id: 'data', dirname: 'data' }, { id: 'kibanaReact', dirname: 'kibana_react' }, { id: 'kibanaUtils', dirname: 'kibana_utils' }, { id: 'esUiShared', dirname: 'es_ui_shared' }, @@ -60,13 +60,8 @@ function dynamicExternals(bundle: Bundle, context: string, request: string) { return; } - // don't allow any static bundle to rely on other static bundles - if (STATIC_BUNDLE_PLUGINS.some(p => bundle.id === p.id)) { - return; - } - - // ignore requests that don't include a /data/public, /kibana_react/public, or - // /kibana_utils/public segment as a cheap way to avoid doing path resolution + // ignore requests that don't include a /{dirname}/public for one of our + // "static" bundles as a cheap way to avoid doing path resolution // for paths that couldn't possibly resolve to what we're looking for const reqToStaticBundle = STATIC_BUNDLE_PLUGINS.some(p => request.includes(`/${p.dirname}/public`) diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 692b787ccf405..9b44395fa9c68 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -145,8 +145,9 @@ export function uiRenderMixin(kbnServer, server, config) { // load these plugins first, they are "shared" and other bundles access their // public/index exports without considering topographic sorting by plugin deps (for now) 'kibanaUtils', - 'esUiShared', 'kibanaReact', + 'data', + 'esUiShared', ...kbnServer.newPlatform.__internals.uiPlugins.public.keys() ); From f8e01bd3a14f606c2086955539e588484b0ab379 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 29 Apr 2020 11:58:46 -0500 Subject: [PATCH 003/122] [SIEM][NP] Fixes bug in ML signals promotion (#64720) * Add set-value as an explicit dependency This is a more robust solution than lodash's set(). * Replace lodash.set() with set-value's equivalent * Rebuild renovate config We added set-value to our dependencies. Co-authored-by: Elastic Machine --- renovate.json5 | 8 ++++++++ x-pack/package.json | 2 ++ .../signals/bulk_create_ml_signals.ts | 10 +++++++--- yarn.lock | 12 ++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/renovate.json5 b/renovate.json5 index ffa006264873d..c0ddcaf4f23c8 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -846,6 +846,14 @@ '@types/semver', ], }, + { + groupSlug: 'set-value', + groupName: 'set-value related packages', + packageNames: [ + 'set-value', + '@types/set-value', + ], + }, { groupSlug: 'sinon', groupName: 'sinon related packages', diff --git a/x-pack/package.json b/x-pack/package.json index 604889c6094b9..dcc9b8c61cb96 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -105,6 +105,7 @@ "@types/recompose": "^0.30.6", "@types/reduce-reducers": "^1.0.0", "@types/redux-actions": "^2.6.1", + "@types/set-value": "^2.0.0", "@types/sinon": "^7.0.13", "@types/styled-components": "^4.4.2", "@types/supertest": "^2.0.5", @@ -344,6 +345,7 @@ "rison-node": "0.3.1", "rxjs": "^6.5.3", "semver": "5.7.0", + "set-value": "^3.0.2", "squel": "^5.13.0", "stats-lite": "^2.2.0", "style-it": "^2.1.3", diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index d298f1cc7cbc6..a8cc6dc680410 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flow, set, omit } from 'lodash/fp'; +import { flow, omit } from 'lodash/fp'; +import set from 'set-value'; import { SearchResponse } from 'elasticsearch'; import { Logger } from '../../../../../../../src/core/server'; @@ -55,8 +56,11 @@ export const transformAnomalyFieldsToEcs = (anomaly: Anomaly): EcsAnomaly => { } const omitDottedFields = omit(errantFields.map(field => field.name)); - const setNestedFields = errantFields.map(field => set(field.name, field.value)); - const setTimestamp = set('@timestamp', new Date(timestamp).toISOString()); + const setNestedFields = errantFields.map(field => (_anomaly: Anomaly) => + set(_anomaly, field.name, field.value) + ); + const setTimestamp = (_anomaly: Anomaly) => + set(_anomaly, '@timestamp', new Date(timestamp).toISOString()); return flow(omitDottedFields, setNestedFields, setTimestamp)(anomaly); }; diff --git a/yarn.lock b/yarn.lock index ee00ef283f07c..94e6a0a11aa99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4798,6 +4798,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== +"@types/set-value@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/set-value/-/set-value-2.0.0.tgz#63d386b103926dcf49b50e16e0f6dd49983046be" + integrity sha512-k8dCJEC80F/mbsIOZ5Hj3YSzTVVVBwMdtP/M9Rtc2TM4F5etVd+2UG8QUiAUfbXm4fABedL2tBZnrBheY7UwpA== + "@types/shot@*": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/shot/-/shot-4.0.0.tgz#7545500c489b65c69b5bc5446ba4fef3bd26af92" @@ -26910,6 +26915,13 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +set-value@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-3.0.2.tgz#74e8ecd023c33d0f77199d415409a40f21e61b90" + integrity sha512-npjkVoz+ank0zjlV9F47Fdbjfj/PfXyVhZvGALWsyIYU/qrMzpi6avjKW3/7KeSU2Df3I46BrN1xOI1+6vW0hA== + dependencies: + is-plain-object "^2.0.4" + setimmediate@^1.0.4, setimmediate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" From bac638a37e5dc670cd1c6c535e1f9fcd77ef8f64 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Wed, 29 Apr 2020 20:00:14 +0300 Subject: [PATCH 004/122] Update jest config for coverage (#64648) * set files to track for coverage collection * increase timeout to 4h * trying to add detectOpenHandles to avoid worker stuck * update config * make config paths more common * update configs * update jest oss config * exclude 'tests' folder for coverage --- .ci/Jenkinsfile_coverage | 2 +- src/dev/jest/config.js | 1 + test/scripts/jenkins_xpack.sh | 2 +- x-pack/dev-tools/jest/create_jest_config.js | 14 ++++++++++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index f2a58e7b6a7ac..c474998e6fd3d 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -3,7 +3,7 @@ library 'kibana-pipeline-library' kibanaLibrary.load() // load from the Jenkins instance -kibanaPipeline(timeoutMinutes: 180) { +kibanaPipeline(timeoutMinutes: 240) { catchErrors { withEnv([ 'CODE_COVERAGE=1', // Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 43a2cbd78c502..c5387590fcf66 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -40,6 +40,7 @@ export default { ], collectCoverageFrom: [ 'src/plugins/**/*.{ts,tsx}', + '!src/plugins/**/{__test__,__snapshots__,__examples__,mocks,tests}/**/*', '!src/plugins/**/*.d.ts', 'packages/kbn-ui-framework/src/components/**/*.js', '!packages/kbn-ui-framework/src/components/index.js', diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 67d88b308ed91..951ba8e22d885 100755 --- a/test/scripts/jenkins_xpack.sh +++ b/test/scripts/jenkins_xpack.sh @@ -39,7 +39,7 @@ else # build runtime for canvas echo "NODE_ENV=$NODE_ENV" node ./legacy/plugins/canvas/scripts/shareable_runtime - node --max-old-space-size=6144 scripts/jest --ci --verbose --coverage + node --max-old-space-size=6144 scripts/jest --ci --verbose --detectOpenHandles --coverage # rename file in order to be unique one test -f ../target/kibana-coverage/jest/coverage-final.json \ && mv ../target/kibana-coverage/jest/coverage-final.json \ diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index af5ace8e3cd3b..4f1251321b005 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -34,6 +34,20 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '^test_utils/stub_web_worker': `${xPackKibanaDirectory}/test_utils/stub_web_worker.ts`, '^(!!)?file-loader!': fileMockPath, }, + collectCoverageFrom: [ + 'legacy/plugins/**/*.{js,jsx,ts,tsx}', + 'legacy/server/**/*.{js,jsx,ts,tsx}', + 'plugins/**/*.{js,jsx,ts,tsx}', + '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', + '!**/*.test.{js,ts,tsx}', + '!**/flot-charts/**', + '!**/test/**', + '!**/build/**', + '!**/scripts/**', + '!**/mocks/**', + '!**/plugins/apm/e2e/**', + ], + coveragePathIgnorePatterns: ['.*\\.d\\.ts'], coverageDirectory: '/../target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html'], setupFiles: [ From d8c15f5ad38c0ed480fc90cf7c8381aad04f0609 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 29 Apr 2020 18:25:48 +0100 Subject: [PATCH 005/122] [ML] Adding endpoint capability checks (#64662) * [ML] Adding endpoint capability checks * adding missing capability checks * fixing test * removing commented code * fixing functional test * fixing functional tests * changes based on review Co-authored-by: Elastic Machine --- .../plugins/ml/common/types/capabilities.ts | 32 +++- .../capabilities/check_capabilities.test.ts | 159 ++++++++++++++---- .../lib/capabilities/check_capabilities.ts | 2 + x-pack/plugins/ml/server/plugin.ts | 14 +- .../plugins/ml/server/routes/annotations.ts | 9 + .../ml/server/routes/anomaly_detectors.ts | 45 +++++ x-pack/plugins/ml/server/routes/calendars.ts | 15 ++ .../ml/server/routes/data_frame_analytics.ts | 33 ++++ .../ml/server/routes/data_visualizer.ts | 6 + x-pack/plugins/ml/server/routes/datafeeds.ts | 30 ++++ .../ml/server/routes/fields_service.ts | 7 +- .../ml/server/routes/file_data_visualizer.ts | 2 + x-pack/plugins/ml/server/routes/filters.ts | 18 ++ x-pack/plugins/ml/server/routes/indices.ts | 3 + .../ml/server/routes/job_audit_messages.ts | 6 + .../plugins/ml/server/routes/job_service.ts | 80 ++++++--- .../ml/server/routes/job_validation.ts | 12 ++ x-pack/plugins/ml/server/routes/modules.ts | 12 ++ .../ml/server/routes/notification_settings.ts | 3 + .../ml/server/routes/results_service.ts | 15 ++ x-pack/plugins/ml/server/routes/system.ts | 16 ++ x-pack/plugins/ml/server/types.ts | 4 - .../apis/ml/anomaly_detectors/create.ts | 9 +- .../apis/ml/jobs/jobs_summary.ts | 4 +- .../apis/ml/modules/setup_module.ts | 7 +- 25 files changed, 456 insertions(+), 87 deletions(-) diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 2a449c95faa5b..da5fd3ac25209 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -7,6 +7,7 @@ import { KibanaRequest } from 'kibana/server'; export const userMlCapabilities = { + canAccessML: false, // Anomaly Detection canGetJobs: false, canGetDatafeeds: false, @@ -18,6 +19,10 @@ export const userMlCapabilities = { canGetFilters: false, // Data Frame Analytics canGetDataFrameAnalytics: false, + // Annotations + canGetAnnotations: false, + canCreateAnnotation: false, + canDeleteAnnotation: false, }; export const adminMlCapabilities = { @@ -26,9 +31,11 @@ export const adminMlCapabilities = { canDeleteJob: false, canOpenJob: false, canCloseJob: false, + canUpdateJob: false, canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, canStartStopDatafeed: false, - canUpdateJob: false, canUpdateDatafeed: false, canPreviewDatafeed: false, // Calendars @@ -38,8 +45,8 @@ export const adminMlCapabilities = { canCreateFilter: false, canDeleteFilter: false, // Data Frame Analytics - canDeleteDataFrameAnalytics: false, canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, canStartStopDataFrameAnalytics: false, }; @@ -47,7 +54,9 @@ export type UserMlCapabilities = typeof userMlCapabilities; export type AdminMlCapabilities = typeof adminMlCapabilities; export type MlCapabilities = UserMlCapabilities & AdminMlCapabilities; -export const basicLicenseMlCapabilities = ['canFindFileStructure'] as Array; +export const basicLicenseMlCapabilities = ['canAccessML', 'canFindFileStructure'] as Array< + keyof MlCapabilities +>; export function getDefaultCapabilities(): MlCapabilities { return { @@ -56,6 +65,23 @@ export function getDefaultCapabilities(): MlCapabilities { }; } +export function getPluginPrivileges() { + const userMlCapabilitiesKeys = Object.keys(userMlCapabilities); + const adminMlCapabilitiesKeys = Object.keys(adminMlCapabilities); + const allMlCapabilities = [...adminMlCapabilitiesKeys, ...userMlCapabilitiesKeys]; + + return { + user: { + ui: userMlCapabilitiesKeys, + api: userMlCapabilitiesKeys.map(k => `ml:${k}`), + }, + admin: { + ui: allMlCapabilities, + api: allMlCapabilities.map(k => `ml:${k}`), + }, + }; +} + export interface MlCapabilitiesResponse { capabilities: MlCapabilities; upgradeInProgress: boolean; diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index 5093801d2d184..b6e95ae8373ee 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -36,7 +36,7 @@ describe('check_capabilities', () => { ); const { capabilities } = await getCapabilities(); const count = Object.keys(capabilities).length; - expect(count).toBe(22); + expect(count).toBe(28); done(); }); }); @@ -49,28 +49,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(true); + expect(capabilities.canDeleteAnnotation).toBe(true); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -84,28 +98,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(true); + expect(capabilities.canDeleteAnnotation).toBe(true); + expect(capabilities.canCreateJob).toBe(true); expect(capabilities.canDeleteJob).toBe(true); expect(capabilities.canOpenJob).toBe(true); expect(capabilities.canCloseJob).toBe(true); expect(capabilities.canForecastJob).toBe(true); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(true); expect(capabilities.canUpdateJob).toBe(true); + expect(capabilities.canCreateDatafeed).toBe(true); + expect(capabilities.canDeleteDatafeed).toBe(true); expect(capabilities.canUpdateDatafeed).toBe(true); expect(capabilities.canPreviewDatafeed).toBe(true); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(true); expect(capabilities.canDeleteCalendar).toBe(true); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(true); expect(capabilities.canDeleteFilter).toBe(true); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); expect(capabilities.canCreateDataFrameAnalytics).toBe(true); expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); @@ -119,28 +147,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(true); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -154,28 +196,42 @@ describe('check_capabilities', () => { mlLicense, mlIsEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(true); expect(mlFeatureEnabledInSpace).toBe(true); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(true); expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canGetAnnotations).toBe(true); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -189,28 +245,42 @@ describe('check_capabilities', () => { mlLicense, mlIsNotEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(false); + expect(isPlatinumOrTrialLicense).toBe(true); + + expect(capabilities.canAccessML).toBe(false); expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetFilters).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canGetAnnotations).toBe(false); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); @@ -225,28 +295,43 @@ describe('check_capabilities', () => { mlLicenseBasic, mlIsNotEnabled ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + const { + capabilities, + upgradeInProgress, + mlFeatureEnabledInSpace, + isPlatinumOrTrialLicense, + } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); expect(mlFeatureEnabledInSpace).toBe(false); + expect(isPlatinumOrTrialLicense).toBe(false); + + expect(capabilities.canAccessML).toBe(false); expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetFilters).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canGetAnnotations).toBe(false); + expect(capabilities.canCreateAnnotation).toBe(false); + expect(capabilities.canDeleteAnnotation).toBe(false); + expect(capabilities.canCreateJob).toBe(false); expect(capabilities.canDeleteJob).toBe(false); expect(capabilities.canOpenJob).toBe(false); expect(capabilities.canCloseJob).toBe(false); expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); expect(capabilities.canStartStopDatafeed).toBe(false); expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canCreateDatafeed).toBe(false); + expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateFilter).toBe(false); expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); expect(capabilities.canCreateDataFrameAnalytics).toBe(false); expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts index a2ad83c5522de..d955cf981faca 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts @@ -44,4 +44,6 @@ function disableAdminPrivileges(capabilities: MlCapabilities) { Object.keys(adminMlCapabilities).forEach(k => { capabilities[k as keyof MlCapabilities] = false; }); + capabilities.canCreateAnnotation = false; + capabilities.canDeleteAnnotation = false; } diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 64f8eb4b0acd3..969b74194148b 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -45,7 +45,7 @@ import { systemRoutes } from './routes/system'; import { MlLicense } from '../common/license'; import { MlServerLicense } from './lib/license'; import { createSharedServices, SharedServices } from './shared_services'; -import { userMlCapabilities, adminMlCapabilities } from '../common/types/capabilities'; +import { getPluginPrivileges } from '../common/types/capabilities'; import { setupCapabilitiesSwitcher } from './lib/capabilities'; import { registerKibanaSettings } from './lib/register_settings'; @@ -75,8 +75,7 @@ export class MlServerPlugin implements Plugin { try { @@ -86,6 +89,9 @@ export function annotationRoutes( validate: { body: indexAnnotationSchema, }, + options: { + tags: ['access:ml:canCreateAnnotation'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -130,6 +136,9 @@ export function annotationRoutes( validate: { params: deleteAnnotationSchema, }, + options: { + tags: ['access:ml:canDeleteAnnotation'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index ca63d69f403f6..63cd5498231af 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -37,6 +37,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -65,6 +68,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -93,6 +99,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/anomaly_detectors/_stats', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -121,6 +130,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -154,6 +166,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: schema.object(anomalyDetectionJobSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -188,6 +203,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: anomalyDetectionUpdateJobSchema, }, + options: { + tags: ['access:ml:canUpdateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -220,6 +238,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canOpenJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -251,6 +272,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canCloseJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -286,6 +310,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canDeleteJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -319,6 +346,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: schema.any(), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -351,6 +381,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: forecastAnomalyDetector, }, + options: { + tags: ['access:ml:canForecastJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -389,6 +422,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: getRecordsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -425,6 +461,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: getBucketParamsSchema, body: getBucketsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -462,6 +501,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { params: jobIdSchema, body: getOverallBucketsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -496,6 +538,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: getCategoriesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts index a17601f74ae93..9c80651a13999 100644 --- a/x-pack/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -52,6 +52,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/calendars', validate: false, + options: { + tags: ['access:ml:canGetCalendars'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -81,6 +84,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { params: calendarIdsSchema, }, + options: { + tags: ['access:ml:canGetCalendars'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { let returnValue; @@ -117,6 +123,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { body: calendarSchema, }, + options: { + tags: ['access:ml:canCreateCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -149,6 +158,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { params: calendarIdSchema, body: calendarSchema, }, + options: { + tags: ['access:ml:canCreateCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -180,6 +192,9 @@ export function calendars({ router, mlLicense }: RouteInitialization) { validate: { params: calendarIdSchema, }, + options: { + tags: ['access:ml:canDeleteCalendar'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index dd9e0ea66aa9d..32cb2b343f876 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -33,6 +33,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat { path: '/api/ml/data_frame/analytics', validate: false, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -61,6 +64,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -88,6 +94,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat { path: '/api/ml/data_frame/analytics/_stats', validate: false, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -118,6 +127,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -155,6 +167,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat params: analyticsIdSchema, body: dataAnalyticsJobConfigSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -190,6 +205,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { body: dataAnalyticsEvaluateSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -224,6 +242,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { body: dataAnalyticsExplainSchema, }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -257,6 +278,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canDeleteDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -291,6 +315,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canStartStopDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -324,6 +351,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat params: analyticsIdSchema, query: stopsDataFrameAnalyticsJobQuerySchema, }, + options: { + tags: ['access:ml:canStartStopDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -364,6 +394,9 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat validate: { params: analyticsIdSchema, }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts index 20029fbd8d1a6..04008a896a1a2 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -88,6 +88,9 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) params: indexPatternTitleSchema, body: dataVisualizerFieldStatsSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -150,6 +153,9 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) params: indexPatternTitleSchema, body: dataVisualizerOverallStatsSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts index ec667e1d305f5..1fa1d408372da 100644 --- a/x-pack/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -28,6 +28,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/datafeeds', validate: false, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -57,6 +60,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -83,6 +89,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/datafeeds/_stats', validate: false, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -112,6 +121,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canGetDatafeeds'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -146,6 +158,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: datafeedConfigSchema, }, + options: { + tags: ['access:ml:canCreateDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -181,6 +196,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: datafeedConfigSchema, }, + options: { + tags: ['access:ml:canUpdateDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -216,6 +234,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, query: deleteDatafeedQuerySchema, }, + options: { + tags: ['access:ml:canDeleteDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -255,6 +276,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { params: datafeedIdSchema, body: startDatafeedSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -291,6 +315,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -324,6 +351,9 @@ export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: datafeedIdSchema, }, + options: { + tags: ['access:ml:canPreviewDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index 577e8e0161342..b0f13df294145 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -46,8 +46,10 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { validate: { body: getCardinalityOfFieldsSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, - mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getCardinalityOfFields(context, request.body); @@ -79,6 +81,9 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { validate: { body: getTimeFieldRangeSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index 3f3fc3f547b6a..0f389f9505943 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -71,6 +71,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat accepts: ['text/*', 'application/json'], maxBytes: MAX_FILE_SIZE_BYTES, }, + tags: ['access:ml:canFindFileStructure'], }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { @@ -105,6 +106,7 @@ export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitializat accepts: ['application/json'], maxBytes: MAX_FILE_SIZE_BYTES, }, + tags: ['access:ml:canFindFileStructure'], }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts index 738c25070358d..d5287c349a8fc 100644 --- a/x-pack/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -57,6 +57,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters', validate: false, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -89,6 +92,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: filterIdSchema, }, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -120,6 +126,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: createFilterSchema, }, + options: { + tags: ['access:ml:canCreateFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -155,6 +164,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { params: filterIdSchema, body: updateFilterSchema, }, + options: { + tags: ['access:ml:canCreateFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -186,6 +198,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: filterIdSchema, }, + options: { + tags: ['access:ml:canDeleteFilter'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -216,6 +231,9 @@ export function filtersRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/filters/_stats', validate: false, + options: { + tags: ['access:ml:canGetFilters'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts index e434936beba63..fb3ef7fc41c76 100644 --- a/x-pack/plugins/ml/server/routes/indices.ts +++ b/x-pack/plugins/ml/server/routes/indices.ts @@ -27,6 +27,9 @@ export function indicesRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: indicesSchema, }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 1fe5a7af95d4f..5acc89e7d13be 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -33,6 +33,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio params: jobAuditMessagesJobIdSchema, query: jobAuditMessagesQuerySchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -67,6 +70,9 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio validate: { query: jobAuditMessagesQuerySchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index 149ca2591fd76..05c44e1da9757 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import { schema } from '@kbn/config-schema'; -import { KibanaRequest } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; -import { RouteInitialization, JobServiceRouteDeps } from '../types'; +import { RouteInitialization } from '../types'; import { categorizationFieldExamplesSchema, chartSchema, @@ -27,19 +25,7 @@ import { categorizationExamplesProvider } from '../models/job_service/new_job'; /** * Routes for job service */ -export function jobServiceRoutes( - { router, mlLicense }: RouteInitialization, - { resolveMlCapabilities }: JobServiceRouteDeps -) { - async function hasPermissionToCreateJobs(request: KibanaRequest) { - const mlCapabilities = await resolveMlCapabilities(request); - if (mlCapabilities === null) { - throw new Error('resolveMlCapabilities is not defined'); - } - - return mlCapabilities.canCreateJob; - } - +export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup JobService * @@ -55,6 +41,9 @@ export function jobServiceRoutes( validate: { body: forceStartDatafeedSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -86,6 +75,9 @@ export function jobServiceRoutes( validate: { body: datafeedIdsSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -117,6 +109,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canDeleteJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -148,6 +143,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canCloseJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -184,6 +182,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -215,6 +216,9 @@ export function jobServiceRoutes( validate: { body: schema.object(jobsWithTimerangeSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -245,6 +249,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -272,6 +279,9 @@ export function jobServiceRoutes( { path: '/api/ml/jobs/groups', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -302,6 +312,9 @@ export function jobServiceRoutes( validate: { body: schema.object(updateGroupsSchema), }, + options: { + tags: ['access:ml:canUpdateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -329,6 +342,9 @@ export function jobServiceRoutes( { path: '/api/ml/jobs/deleting_jobs_tasks', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -359,6 +375,9 @@ export function jobServiceRoutes( validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -389,6 +408,9 @@ export function jobServiceRoutes( params: schema.object({ indexPattern: schema.string() }), query: schema.maybe(schema.object({ rollup: schema.maybe(schema.string()) })), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -422,6 +444,9 @@ export function jobServiceRoutes( validate: { body: schema.object(chartSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -474,6 +499,9 @@ export function jobServiceRoutes( validate: { body: schema.object(chartSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -522,6 +550,9 @@ export function jobServiceRoutes( { path: '/api/ml/jobs/all_jobs_and_group_ids', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -552,6 +583,9 @@ export function jobServiceRoutes( validate: { body: schema.object(lookBackProgressSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -583,17 +617,12 @@ export function jobServiceRoutes( validate: { body: schema.object(categorizationFieldExamplesSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { - // due to the use of the _analyze endpoint which is called by the kibana user, - // basic job creation privileges are required to use this endpoint - if ((await hasPermissionToCreateJobs(request)) === false) { - throw Boom.forbidden( - 'Insufficient privileges, the machine_learning_admin role is required.' - ); - } - const { validateCategoryExamples } = categorizationExamplesProvider( context.ml!.mlClient.callAsCurrentUser, context.ml!.mlClient.callAsInternalUser @@ -644,6 +673,9 @@ export function jobServiceRoutes( validate: { body: schema.object(topCategoriesSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index dd2bd9deadf43..632166d6d5fb8 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -57,6 +57,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: estimateBucketSpanSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -106,6 +109,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: modelMemoryLimitSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -135,6 +141,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: validateCardinalitySchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -167,6 +176,9 @@ export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, validate: { body: validateJobSchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index 2891144fc4574..622ae66ede426 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -97,6 +97,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { indexPatternTitle: schema.string(), }), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -127,6 +130,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { ...getModuleIdParamSchema(true), }), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -161,6 +167,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { params: schema.object(getModuleIdParamSchema()), body: setupModuleBodySchema, }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -218,6 +227,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { validate: { params: schema.object(getModuleIdParamSchema()), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/notification_settings.ts b/x-pack/plugins/ml/server/routes/notification_settings.ts index 59458b1e486db..e4a9abb0784be 100644 --- a/x-pack/plugins/ml/server/routes/notification_settings.ts +++ b/x-pack/plugins/ml/server/routes/notification_settings.ts @@ -22,6 +22,9 @@ export function notificationRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/notification_settings', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 89c267340fe52..94ca0827ccfa5 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -88,6 +88,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: anomaliesTableDataSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -117,6 +120,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: categoryDefinitionSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -146,6 +152,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: maxAnomalyScoreSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -175,6 +184,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: categoryExamplesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -204,6 +216,9 @@ export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) validate: { body: partitionFieldValuesSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index d5fe45728c56c..7ae7dd8eef065 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -54,6 +54,9 @@ export function systemRoutes( validate: { body: schema.maybe(schema.any()), }, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -110,6 +113,9 @@ export function systemRoutes( { path: '/api/ml/ml_capabilities', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -150,7 +156,11 @@ export function systemRoutes( { path: '/api/ml/ml_node_count', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { // check for basic license first for consistency with other @@ -201,6 +211,9 @@ export function systemRoutes( { path: '/api/ml/info', validate: false, + options: { + tags: ['access:ml:canAccessML'], + }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { @@ -229,6 +242,9 @@ export function systemRoutes( validate: { body: schema.maybe(schema.any()), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index d4cd61a7fa4f7..678e81d3526ac 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -30,10 +30,6 @@ export interface SystemRouteDeps { resolveMlCapabilities: ResolveMlCapabilities; } -export interface JobServiceRouteDeps { - resolveMlCapabilities: ResolveMlCapabilities; -} - export interface PluginsSetup { cloud: CloudSetup; features: FeaturesPluginSetup; diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts index bbc766df34dcf..10857caab98e2 100644 --- a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts @@ -91,12 +91,11 @@ export default ({ getService }: FtrProviderContext) => { model_plot_config: { enabled: true }, }, expected: { - responseCode: 403, + responseCode: 404, responseBody: { - statusCode: 403, - error: 'Forbidden', - message: - '[security_exception] action [cluster:admin/xpack/ml/job/put] is unauthorized for user [ml_viewer]', + statusCode: 404, + error: 'Not Found', + message: 'Not Found', }, }, }, diff --git a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts index 6a57db1687868..a5cb68d782126 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts @@ -197,8 +197,8 @@ export default ({ getService }: FtrProviderContext) => { requestBody: {}, // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic. expected: { - responseCode: 403, - error: 'Forbidden', + responseCode: 404, + error: 'Not Found', }, }, ]; diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index 23ddd3b63a2ef..c42fc95c1bc7f 100644 --- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -84,10 +84,9 @@ export default ({ getService }: FtrProviderContext) => { startDatafeed: false, }, expected: { - responseCode: 403, - error: 'Forbidden', - message: - '[security_exception] action [cluster:monitor/xpack/ml/info/get] is unauthorized for user [ml_unauthorized]', + responseCode: 404, + error: 'Not Found', + message: 'Not Found', }, }, ]; From 1512859e56d5196665ffbfaefd9b7a25d8b24100 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Wed, 29 Apr 2020 10:54:19 -0700 Subject: [PATCH 006/122] [Telemetry] oss api tests (#64602) * Adds telemetry API tests for oss * Modifies test expectations to match that within oss Co-authored-by: Elastic Machine --- test/api_integration/apis/index.js | 1 + test/api_integration/apis/telemetry/index.js | 26 ++++ test/api_integration/apis/telemetry/opt_in.ts | 123 ++++++++++++++++ .../apis/telemetry/telemetry_local.js | 133 ++++++++++++++++++ .../telemetry/telemetry_optin_notice_seen.ts | 59 ++++++++ 5 files changed, 342 insertions(+) create mode 100644 test/api_integration/apis/telemetry/index.js create mode 100644 test/api_integration/apis/telemetry/opt_in.ts create mode 100644 test/api_integration/apis/telemetry/telemetry_local.js create mode 100644 test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index c5bfc847d0041..0c4028905657d 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -33,5 +33,6 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./status')); loadTestFile(require.resolve('./stats')); loadTestFile(require.resolve('./ui_metric')); + loadTestFile(require.resolve('./telemetry')); }); } diff --git a/test/api_integration/apis/telemetry/index.js b/test/api_integration/apis/telemetry/index.js new file mode 100644 index 0000000000000..c79f5cb470890 --- /dev/null +++ b/test/api_integration/apis/telemetry/index.js @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export default function({ loadTestFile }) { + describe('Telemetry', () => { + loadTestFile(require.resolve('./telemetry_local')); + loadTestFile(require.resolve('./opt_in')); + loadTestFile(require.resolve('./telemetry_optin_notice_seen')); + }); +} diff --git a/test/api_integration/apis/telemetry/opt_in.ts b/test/api_integration/apis/telemetry/opt_in.ts new file mode 100644 index 0000000000000..e4654ee3985f3 --- /dev/null +++ b/test/api_integration/apis/telemetry/opt_in.ts @@ -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 expect from '@kbn/expect'; + +import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function optInTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + describe('/api/telemetry/v2/optIn API', () => { + let defaultAttributes: TelemetrySavedObjectAttributes; + let kibanaVersion: any; + before(async () => { + const kibanaVersionAccessor = kibanaServer.version; + kibanaVersion = await kibanaVersionAccessor.get(); + defaultAttributes = + (await getSavedObjectAttributes(supertest).catch(err => { + if (err.message === 'expected 200 "OK", got 404 "Not Found"') { + return null; + } + throw err; + })) || {}; + + expect(typeof kibanaVersion).to.eql('string'); + expect(kibanaVersion.length).to.be.greaterThan(0); + }); + + afterEach(async () => { + await updateSavedObjectAttributes(supertest, defaultAttributes); + }); + + it('should support sending false with allowChangingOptInStatus true', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: true, + }); + await postTelemetryV2Optin(supertest, false, 200); + const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest); + expect(enabled).to.be(false); + expect(lastVersionChecked).to.be(kibanaVersion); + }); + + it('should support sending true with allowChangingOptInStatus true', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: true, + }); + await postTelemetryV2Optin(supertest, true, 200); + const { enabled, lastVersionChecked } = await getSavedObjectAttributes(supertest); + expect(enabled).to.be(true); + expect(lastVersionChecked).to.be(kibanaVersion); + }); + + it('should not support sending false with allowChangingOptInStatus false', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: false, + }); + await postTelemetryV2Optin(supertest, false, 400); + }); + + it('should not support sending true with allowChangingOptInStatus false', async () => { + await updateSavedObjectAttributes(supertest, { + ...defaultAttributes, + allowChangingOptInStatus: false, + }); + await postTelemetryV2Optin(supertest, true, 400); + }); + + it('should not support sending null', async () => { + await postTelemetryV2Optin(supertest, null, 400); + }); + + it('should not support sending junk', async () => { + await postTelemetryV2Optin(supertest, 42, 400); + }); + }); +} + +async function postTelemetryV2Optin(supertest: any, value: any, statusCode: number): Promise { + const { body } = await supertest + .post('/api/telemetry/v2/optIn') + .set('kbn-xsrf', 'xxx') + .send({ enabled: value }) + .expect(statusCode); + + return body; +} + +async function updateSavedObjectAttributes( + supertest: any, + attributes: TelemetrySavedObjectAttributes +): Promise { + return await supertest + .post('/api/saved_objects/telemetry/telemetry') + .query({ overwrite: true }) + .set('kbn-xsrf', 'xxx') + .send({ attributes }) + .expect(200); +} + +async function getSavedObjectAttributes(supertest: any): Promise { + const { body } = await supertest.get('/api/saved_objects/telemetry/telemetry').expect(200); + return body.attributes; +} diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js new file mode 100644 index 0000000000000..84bfd8a755c11 --- /dev/null +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import _ from 'lodash'; + +/* + * Create a single-level array with strings for all the paths to values in the + * source object, up to 3 deep. Going deeper than 3 causes a bit too much churn + * in the tests. + */ +function flatKeys(source) { + const recursivelyFlatKeys = (obj, path = [], depth = 0) => { + return depth < 3 && _.isObject(obj) + ? _.map(obj, (v, k) => recursivelyFlatKeys(v, [...path, k], depth + 1)) + : path.join('.'); + }; + + return _.uniq(_.flattenDeep(recursivelyFlatKeys(source))).sort((a, b) => a.localeCompare(b)); +} + +export default function({ getService }) { + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/clusters/_stats', () => { + it('should pull local stats and validate data types', async () => { + const timeRange = { + min: '2018-07-23T22:07:00Z', + max: '2018-07-23T22:13:00Z', + }; + + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, unencrypted: true }) + .expect(200); + + expect(body.length).to.be(1); + const stats = body[0]; + expect(stats.collection).to.be('local'); + expect(stats.stack_stats.kibana.count).to.be.a('number'); + expect(stats.stack_stats.kibana.indices).to.be.a('number'); + expect(stats.stack_stats.kibana.os.platforms[0].platform).to.be.a('string'); + expect(stats.stack_stats.kibana.os.platforms[0].count).to.be(1); + expect(stats.stack_stats.kibana.os.platformReleases[0].platformRelease).to.be.a('string'); + expect(stats.stack_stats.kibana.os.platformReleases[0].count).to.be(1); + expect(stats.stack_stats.kibana.plugins.telemetry.opt_in_status).to.be(false); + expect(stats.stack_stats.kibana.plugins.telemetry.usage_fetcher).to.be.a('string'); + expect(stats.stack_stats.kibana.plugins.stack_management).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.ui_metric).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.application_usage).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string'); + expect(stats.stack_stats.kibana.plugins['tsvb-validation']).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.localization).to.be.an('object'); + expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true); + expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true); + expect(stats.stack_stats.kibana.plugins.csp.rulesChangedFromDefault).to.be(false); + }); + + it('should pull local stats and validate fields', async () => { + const timeRange = { + min: '2018-07-23T22:07:00Z', + max: '2018-07-23T22:13:00Z', + }; + + const { body } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ timeRange, unencrypted: true }) + .expect(200); + + const stats = body[0]; + + const actual = flatKeys(stats); + expect(actual).to.be.an('array'); + const expected = [ + 'cluster_name', + 'cluster_stats.cluster_uuid', + 'cluster_stats.indices.analysis', + 'cluster_stats.indices.completion', + 'cluster_stats.indices.count', + 'cluster_stats.indices.docs', + 'cluster_stats.indices.fielddata', + 'cluster_stats.indices.mappings', + 'cluster_stats.indices.query_cache', + 'cluster_stats.indices.segments', + 'cluster_stats.indices.shards', + 'cluster_stats.indices.store', + 'cluster_stats.nodes.count', + 'cluster_stats.nodes.discovery_types', + 'cluster_stats.nodes.fs', + 'cluster_stats.nodes.ingest', + 'cluster_stats.nodes.jvm', + 'cluster_stats.nodes.network_types', + 'cluster_stats.nodes.os', + 'cluster_stats.nodes.packaging_types', + 'cluster_stats.nodes.plugins', + 'cluster_stats.nodes.process', + 'cluster_stats.nodes.versions', + 'cluster_stats.status', + 'cluster_stats.timestamp', + 'cluster_uuid', + 'collection', + 'collectionSource', + 'stack_stats.kibana.count', + 'stack_stats.kibana.indices', + 'stack_stats.kibana.os', + 'stack_stats.kibana.plugins', + 'stack_stats.kibana.versions', + 'timestamp', + 'version', + ]; + + expect(expected.every(m => actual.includes(m))).to.be.ok(); + }); + }); +} diff --git a/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts new file mode 100644 index 0000000000000..4413c672fb46c --- /dev/null +++ b/test/api_integration/apis/telemetry/telemetry_optin_notice_seen.ts @@ -0,0 +1,59 @@ +/* + * 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 expect from '@kbn/expect'; +import { Client, DeleteDocumentParams, GetParams, GetResponse } from 'elasticsearch'; +import { TelemetrySavedObjectAttributes } from 'src/plugins/telemetry/server/telemetry_repository'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function optInTest({ getService }: FtrProviderContext) { + const client: Client = getService('legacyEs'); + const supertest = getService('supertest'); + + describe('/api/telemetry/v2/userHasSeenNotice API Telemetry User has seen OptIn Notice', () => { + it('should update telemetry setting field via PUT', async () => { + try { + await client.delete({ + index: '.kibana', + id: 'telemetry:telemetry', + } as DeleteDocumentParams); + } catch (err) { + if (err.statusCode !== 404) { + throw err; + } + } + + await supertest + .put('/api/telemetry/v2/userHasSeenNotice') + .set('kbn-xsrf', 'xxx') + .expect(200); + + const { + _source: { telemetry }, + }: GetResponse<{ + telemetry: TelemetrySavedObjectAttributes; + }> = await client.get({ + index: '.kibana', + id: 'telemetry:telemetry', + } as GetParams); + + expect(telemetry.userHasSeenNotice).to.be(true); + }); + }); +} From 3adab851380f4a368b9aa14cb227ef9f61e16e93 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 29 Apr 2020 13:01:23 -0500 Subject: [PATCH 007/122] Minimize dependencies required by our telemetry middleware (#64665) 1. we need our redux actions for our telemetry middleware, which: 2. require types from an index file, which: 3. includes all of our model types, and everything involved in them By moving these types to a separate file and importing _that_instead, we bypass inclusion of 2 and 3 in our plugin, which equates to ~550kB (of a total of ~600kB). --- .../siem/public/lib/telemetry/middleware.ts | 2 +- x-pack/plugins/siem/public/store/model.ts | 13 +------------ .../siem/public/store/timeline/actions.ts | 2 +- x-pack/plugins/siem/public/store/types.ts | 17 +++++++++++++++++ 4 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/siem/public/store/types.ts diff --git a/x-pack/plugins/siem/public/lib/telemetry/middleware.ts b/x-pack/plugins/siem/public/lib/telemetry/middleware.ts index 59c6cb3566907..ca889e20e695f 100644 --- a/x-pack/plugins/siem/public/lib/telemetry/middleware.ts +++ b/x-pack/plugins/siem/public/lib/telemetry/middleware.ts @@ -7,7 +7,7 @@ import { Action, Dispatch, MiddlewareAPI } from 'redux'; import { track, METRIC_TYPE, TELEMETRY_EVENT } from './'; -import { timelineActions } from '../../store/actions'; +import * as timelineActions from '../../store/timeline/actions'; export const telemetryMiddleware = (api: MiddlewareAPI) => (next: Dispatch) => (action: Action) => { if (timelineActions.endTimelineSaving.match(action)) { diff --git a/x-pack/plugins/siem/public/store/model.ts b/x-pack/plugins/siem/public/store/model.ts index 9e9e663a59fe0..686dc096e61b0 100644 --- a/x-pack/plugins/siem/public/store/model.ts +++ b/x-pack/plugins/siem/public/store/model.ts @@ -9,15 +9,4 @@ export { dragAndDropModel } from './drag_and_drop'; export { hostsModel } from './hosts'; export { inputsModel } from './inputs'; export { networkModel } from './network'; - -export type KueryFilterQueryKind = 'kuery' | 'lucene'; - -export interface KueryFilterQuery { - kind: KueryFilterQueryKind; - expression: string; -} - -export interface SerializedFilterQuery { - kuery: KueryFilterQuery | null; - serializedQuery: string; -} +export * from './types'; diff --git a/x-pack/plugins/siem/public/store/timeline/actions.ts b/x-pack/plugins/siem/public/store/timeline/actions.ts index a03cc2643e014..12155decf40d4 100644 --- a/x-pack/plugins/siem/public/store/timeline/actions.ts +++ b/x-pack/plugins/siem/public/store/timeline/actions.ts @@ -12,7 +12,7 @@ import { DataProvider, QueryOperator, } from '../../components/timeline/data_providers/data_provider'; -import { KueryFilterQuery, SerializedFilterQuery } from '../model'; +import { KueryFilterQuery, SerializedFilterQuery } from '../types'; import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; import { TimelineNonEcsData } from '../../graphql/types'; diff --git a/x-pack/plugins/siem/public/store/types.ts b/x-pack/plugins/siem/public/store/types.ts new file mode 100644 index 0000000000000..2c679ba41116e --- /dev/null +++ b/x-pack/plugins/siem/public/store/types.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. + */ + +export type KueryFilterQueryKind = 'kuery' | 'lucene'; + +export interface KueryFilterQuery { + kind: KueryFilterQueryKind; + expression: string; +} + +export interface SerializedFilterQuery { + kuery: KueryFilterQuery | null; + serializedQuery: string; +} From 53ff22997a1401ccbd2808831b26b92fa174de92 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Wed, 29 Apr 2020 14:06:08 -0400 Subject: [PATCH 008/122] [EPM] Update UI to handle package versions and updates (#64689) * link to installed version of detail page * add latestVersion property to EPM get package endpoint * add updates available notices * add update package button * handle various states and send installedVersion from package endpoint * fix type errors * fix install error because not returning promises * track version in state * handle unsuccessful update attempt * remove unused variable --- .../common/services/package_to_config.test.ts | 1 + .../ingest_manager/common/types/models/epm.ts | 2 + .../sections/epm/components/icons.tsx | 15 ++ .../sections/epm/components/package_card.tsx | 8 +- .../epm/hooks/use_package_install.tsx | 63 +++-- .../sections/epm/screens/detail/content.tsx | 3 +- .../epm/screens/detail/data_sources_panel.tsx | 2 +- .../sections/epm/screens/detail/header.tsx | 17 +- .../sections/epm/screens/detail/index.tsx | 4 +- .../screens/detail/installation_button.tsx | 23 +- .../epm/screens/detail/settings_panel.tsx | 236 ++++++++++++------ .../epm/screens/detail/side_nav_links.tsx | 2 +- .../server/services/epm/packages/get.ts | 4 +- .../server/services/epm/packages/index.ts | 1 + .../server/services/epm/packages/install.ts | 2 +- .../server/services/epm/packages/remove.ts | 8 +- 16 files changed, 277 insertions(+), 114 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts index cb290e61b17e5..a977a1a66e059 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts @@ -11,6 +11,7 @@ describe('Ingest Manager - packageToConfig', () => { name: 'mock-package', title: 'Mock package', version: '0.0.0', + latestVersion: '0.0.0', description: 'description', type: 'mock', categories: [], diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 05e160cdfb81a..f8779a879a049 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -204,6 +204,8 @@ export interface RegistryVarsEntry { // internal until we need them interface PackageAdditions { title: string; + latestVersion: string; + installedVersion?: string; assets: AssetsGroupedByServiceByType; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx new file mode 100644 index 0000000000000..64223efefaab8 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx @@ -0,0 +1,15 @@ +/* + * 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 { EuiIcon } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +export const StyledAlert = styled(EuiIcon)` + color: ${props => props.theme.eui.euiColorWarning}; + padding: 0 5px; +`; + +export const UpdateIcon = () => ; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx index 8ad081cbbabe4..ab7e87b3ad06c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx @@ -30,9 +30,15 @@ export function PackageCard({ showInstalledBadge, status, icons, + ...restProps }: PackageCardProps) { const { toDetailView } = useLinks(); - const url = toDetailView({ name, version }); + let urlVersion = version; + // if this is an installed package, link to the version installed + if ('savedObject' in restProps) { + urlVersion = restProps.savedObject.attributes.version || version; + } + const url = toDetailView({ name, version: urlVersion }); return ( ; +type InstallPackageProps = Pick & { + fromUpdate?: boolean; +}; +type SetPackageInstallStatusProps = Pick & PackageInstallItem; function usePackageInstall({ notifications }: { notifications: NotificationsStart }) { + const { toDetailView } = useLinks(); const [packages, setPackage] = useState({}); const setPackageInstallStatus = useCallback( - ({ name, status }: { name: PackageInfo['name']; status: InstallStatus }) => { + ({ name, status, version }: SetPackageInstallStatusProps) => { + const packageProps: PackageInstallItem = { + status, + version, + }; setPackage((prev: PackagesInstall) => ({ ...prev, - [name]: { status }, + [name]: packageProps, })); }, [] ); + const getPackageInstallStatus = useCallback( + (pkg: string): PackageInstallItem => { + return packages[pkg]; + }, + [packages] + ); + const installPackage = useCallback( - async ({ name, version, title }: InstallPackageProps) => { - setPackageInstallStatus({ name, status: InstallStatus.installing }); + async ({ name, version, title, fromUpdate = false }: InstallPackageProps) => { + const currStatus = getPackageInstallStatus(name); + const newStatus = { ...currStatus, name, status: InstallStatus.installing }; + setPackageInstallStatus(newStatus); const pkgkey = `${name}-${version}`; const res = await sendInstallPackage(pkgkey); if (res.error) { - setPackageInstallStatus({ name, status: InstallStatus.notInstalled }); + if (fromUpdate) { + // if there is an error during update, set it back to the previous version + // as handling of bad update is not implemented yet + setPackageInstallStatus({ ...currStatus, name }); + } else { + setPackageInstallStatus({ name, status: InstallStatus.notInstalled, version }); + } notifications.toasts.addWarning({ title: toMountPoint( { - return packages[pkg].status; - }, - [packages] + [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, toDetailView] ); const uninstallPackage = useCallback( async ({ name, version, title }: Pick) => { - setPackageInstallStatus({ name, status: InstallStatus.uninstalling }); + setPackageInstallStatus({ name, status: InstallStatus.uninstalling, version }); const pkgkey = `${name}-${version}`; const res = await sendRemovePackage(pkgkey); if (res.error) { - setPackageInstallStatus({ name, status: InstallStatus.installed }); + setPackageInstallStatus({ name, status: InstallStatus.installed, version }); notifications.toasts.addWarning({ title: toMountPoint( ; export function ContentPanel(props: ContentPanelProps) { - const { panel, name, version, assets, title, removable } = props; + const { panel, name, version, assets, title, removable, latestVersion } = props; switch (panel) { case 'settings': return ( @@ -60,6 +60,7 @@ export function ContentPanel(props: ContentPanelProps) { assets={assets} title={title} removable={removable} + latestVersion={latestVersion} /> ); case 'data-sources': diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx index fa3245aec02c5..c82b7ed2297a7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx @@ -20,7 +20,7 @@ export const DataSourcesPanel = ({ name, version }: DataSourcesPanelProps) => { const packageInstallStatus = getPackageInstallStatus(name); // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab - if (packageInstallStatus !== InstallStatus.installed) + if (packageInstallStatus.status !== InstallStatus.installed) return ( props.theme.eui.euiSizeM}; `; -const StyledVersion = styled(Version)` - font-size: ${props => props.theme.eui.euiFontSizeS}; - color: ${props => props.theme.eui.euiColorDarkShade}; -`; - type HeaderProps = PackageInfo & { iconType?: IconType }; export function Header(props: HeaderProps) { - const { iconType, name, title, version } = props; + const { iconType, name, title, version, installedVersion, latestVersion } = props; const hasWriteCapabilites = useCapabilities().write; const { toListView } = useLinks(); const ADD_DATASOURCE_URI = useLink(`${EPM_PATH}/${name}-${version}/add-datasource`); - + const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; return ( @@ -59,7 +54,11 @@ export function Header(props: HeaderProps) {

{title} - + + + {version} {updateAvailable && } + +

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx index 3239d7b90e3c3..1f3eb2cc9362e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx @@ -32,11 +32,12 @@ export function Detail() { const packageInfo = response.data?.response; const title = packageInfo?.title; const name = packageInfo?.name; + const installedVersion = packageInfo?.installedVersion; const status: InstallStatus = packageInfo?.status as any; // track install status state if (name) { - setPackageInstallStatus({ name, status }); + setPackageInstallStatus({ name, status, version: installedVersion || null }); } if (packageInfo) { setInfo({ ...packageInfo, title: title || '' }); @@ -64,7 +65,6 @@ type LayoutProps = PackageInfo & Pick & Pick diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx index cbbf1ce53c4ea..cdad67fd87548 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/installation_button.tsx @@ -13,19 +13,21 @@ import { ConfirmPackageUninstall } from './confirm_package_uninstall'; import { ConfirmPackageInstall } from './confirm_package_install'; type InstallationButtonProps = Pick & { - disabled: boolean; + disabled?: boolean; + isUpdate?: boolean; }; export function InstallationButton(props: InstallationButtonProps) { - const { assets, name, title, version, disabled = true } = props; + const { assets, name, title, version, disabled = true, isUpdate = false } = props; const hasWriteCapabilites = useCapabilities().write; const installPackage = useInstallPackage(); const uninstallPackage = useUninstallPackage(); const getPackageInstallStatus = useGetPackageInstallStatus(); - const installationStatus = getPackageInstallStatus(name); + const { status: installationStatus } = getPackageInstallStatus(name); const isInstalling = installationStatus === InstallStatus.installing; const isRemoving = installationStatus === InstallStatus.uninstalling; const isInstalled = installationStatus === InstallStatus.installed; + const showUninstallButton = isInstalled || isRemoving; const [isModalVisible, setModalVisible] = useState(false); const toggleModal = useCallback(() => { setModalVisible(!isModalVisible); @@ -36,6 +38,10 @@ export function InstallationButton(props: InstallationButtonProps) { toggleModal(); }, [installPackage, name, title, toggleModal, version]); + const handleClickUpdate = useCallback(() => { + installPackage({ name, version, title, fromUpdate: true }); + }, [installPackage, name, title, version]); + const handleClickUninstall = useCallback(() => { uninstallPackage({ name, version, title }); toggleModal(); @@ -78,6 +84,15 @@ export function InstallationButton(props: InstallationButtonProps) { ); + const updateButton = ( + + + + ); + const uninstallButton = ( - {isInstalled || isRemoving ? uninstallButton : installButton} + {isUpdate ? updateButton : showUninstallButton ? uninstallButton : installButton} {isModalVisible && (isInstalled ? uninstallModal : installModal)}
) : null; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx index 3589a1a9444e1..4d4dba2a64e5a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx @@ -8,11 +8,22 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; +import styled from 'styled-components'; import { InstallStatus, PackageInfo } from '../../../../types'; import { useGetDatasources } from '../../../../hooks'; import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../constants'; import { useGetPackageInstallStatus } from '../../hooks'; import { InstallationButton } from './installation_button'; +import { UpdateIcon } from '../../components/icons'; + +const SettingsTitleCell = styled.td` + padding-right: ${props => props.theme.eui.spacerSizes.xl}; + padding-bottom: ${props => props.theme.eui.spacerSizes.m}; +`; + +const UpdatesAvailableMsgContainer = styled.span` + padding-left: ${props => props.theme.eui.spacerSizes.s}; +`; const NoteLabel = () => ( ( defaultMessage="Note:" /> ); +const UpdatesAvailableMsg = () => ( + + + + +); + export const SettingsPanel = ( - props: Pick + props: Pick ) => { + const { name, title, removable, latestVersion, version } = props; const getPackageInstallStatus = useGetPackageInstallStatus(); const { data: datasourcesData } = useGetDatasources({ perPage: 0, page: 1, kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name:${props.name}`, }); - const { name, title, removable } = props; - const packageInstallStatus = getPackageInstallStatus(name); + const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name); const packageHasDatasources = !!datasourcesData?.total; + const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; + const isViewingOldPackage = version < latestVersion; + // hide install/remove options if the user has version of the package is installed + // and this package is out of date or if they do have a version installed but it's not this one + const hideInstallOptions = + (installationStatus === InstallStatus.notInstalled && isViewingOldPackage) || + (installationStatus === InstallStatus.installed && installedVersion !== version); + + const isUpdating = installationStatus === InstallStatus.installing && installedVersion; return ( @@ -43,14 +73,13 @@ export const SettingsPanel = ( - {packageInstallStatus === InstallStatus.notInstalled || - packageInstallStatus === InstallStatus.installing ? ( + {installedVersion !== null && (

-

- -

+ + + + + + + + + + + + + + + +
+ + {installedVersion} + + {updateAvailable && } +
+ + {latestVersion} + +
+ {updateAvailable && ( +

+ +

+ )}

- ) : ( + )} + {!hideInstallOptions && !isUpdating && (
- -

+ + {installationStatus === InstallStatus.notInstalled || + installationStatus === InstallStatus.installing ? ( +
+ +

+ +

+
+ +

+ +

+
+ ) : ( +
+ +

+ +

+
+ +

+ +

+
+ )} + + +

+ +

+
+
+ {packageHasDatasources && removable === true && ( +

+ + + ), }} /> -

-
- -

- -

+

+ )} + {removable === false && ( +

+ + + + ), + }} + /> +

+ )}
)} - - -

- -

-
-
- {packageHasDatasources && removable === true && ( -

- - - - ), - }} - /> -

- )} - {removable === false && ( -

- - - - ), - }} - /> -

- )}
); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx index 05729ccfc1af4..ab168ef1530bd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx @@ -37,7 +37,7 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) { : p.theme.eui.euiFontWeightRegular}; `; // don't display Data Sources tab if the package is not installed - if (packageInstallStatus !== InstallStatus.installed && panel === 'data-sources') + if (packageInstallStatus.status !== InstallStatus.installed && panel === 'data-sources') return null; return ( diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index d76584225877c..da8d79a04b97c 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -67,9 +67,10 @@ export async function getPackageInfo(options: { pkgVersion: string; }): Promise { const { savedObjectsClient, pkgName, pkgVersion } = options; - const [item, savedObject, assets] = await Promise.all([ + const [item, savedObject, latestPackage, assets] = await Promise.all([ Registry.fetchInfo(pkgName, pkgVersion), getInstallationObject({ savedObjectsClient, pkgName }), + Registry.fetchFindLatestPackage(pkgName), Registry.getArchiveInfo(pkgName, pkgVersion), ] as const); // adding `as const` due to regression in TS 3.7.2 @@ -79,6 +80,7 @@ export async function getPackageInfo(options: { // add properties that aren't (or aren't yet) on Registry response const updated = { ...item, + latestVersion: latestPackage.version, title: item.title || nameAsTitle(item.name), assets: Registry.groupPathsByService(assets || []), }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index d49e0e661440f..c67cccd044bf5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -43,6 +43,7 @@ export function createInstallableFrom( ? { ...from, status: InstallationStatus.installed, + installedVersion: savedObject.attributes.version, savedObject, } : { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 06f3decdbbe6f..8f51c4d78305c 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -106,7 +106,7 @@ export async function installPackage(options: { try { await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed); } catch (err) { - // some assets may not exist if deleting during a failed update + // log these errors, some assets may not exist if deleted during a failed update } } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index ac2c869f3b9e9..befb4722b6504 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -121,8 +121,12 @@ export async function deleteKibanaSavedObjectsAssets( const deletePromises = installedObjects.map(({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { - savedObjectsClient.delete(assetType, id); + return savedObjectsClient.delete(assetType, id); } }); - await Promise.all(deletePromises); + try { + await Promise.all(deletePromises); + } catch (err) { + throw new Error('error deleting saved object asset'); + } } From f5a8d8e6c27958dacc16a36b24bded57b4313f9d Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Wed, 29 Apr 2020 19:05:45 +0000 Subject: [PATCH 009/122] make inserting timestamp with navigate methods optional with default true (#64655) --- test/functional/page_objects/common_page.ts | 23 +++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 862e5127bb670..93debdcc37f0a 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -44,6 +44,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl: boolean; shouldLoginIfPrompted: boolean; useActualUrl: boolean; + insertTimestamp: boolean; } class CommonPage { @@ -65,7 +66,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo * Logins to Kibana as default user and navigates to provided app * @param appUrl Kibana URL */ - private async loginIfPrompted(appUrl: string) { + private async loginIfPrompted(appUrl: string, insertTimestamp: boolean) { let currentUrl = await browser.getCurrentUrl(); log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`); await testSubjects.find('kibanaChrome', 6 * defaultFindTimeout); // 60 sec waiting @@ -87,7 +88,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo '[data-test-subj="kibanaChrome"] nav:not(.ng-hide)', 6 * defaultFindTimeout ); - await browser.get(appUrl); + await browser.get(appUrl, insertTimestamp); currentUrl = await browser.getCurrentUrl(); log.debug(`Finished login process currentUrl = ${currentUrl}`); } @@ -95,7 +96,13 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } private async navigate(navigateProps: NavigateProps) { - const { appConfig, ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl } = navigateProps; + const { + appConfig, + ensureCurrentUrl, + shouldLoginIfPrompted, + useActualUrl, + insertTimestamp, + } = navigateProps; const appUrl = getUrl.noAuth(config.get('servers.kibana'), appConfig); await retry.try(async () => { @@ -111,7 +118,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } const currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl) + ? await this.loginIfPrompted(appUrl, insertTimestamp) : await browser.getCurrentUrl(); if (ensureCurrentUrl && !currentUrl.includes(appUrl)) { @@ -134,6 +141,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl = true, shouldLoginIfPrompted = true, useActualUrl = false, + insertTimestamp = true, } = {} ) { const appConfig = { @@ -146,6 +154,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl, + insertTimestamp, }); } @@ -165,6 +174,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl = true, shouldLoginIfPrompted = true, useActualUrl = true, + insertTimestamp = true, } = {} ) { const appConfig = { @@ -178,6 +188,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo ensureCurrentUrl, shouldLoginIfPrompted, useActualUrl, + insertTimestamp, }); } @@ -208,7 +219,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo async navigateToApp( appName: string, - { basePath = '', shouldLoginIfPrompted = true, hash = '' } = {} + { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {} ) { let appUrl: string; if (config.has(['apps', appName])) { @@ -239,7 +250,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo log.debug('returned from get, calling refresh'); await browser.refresh(); let currentUrl = shouldLoginIfPrompted - ? await this.loginIfPrompted(appUrl) + ? await this.loginIfPrompted(appUrl, insertTimestamp) : await browser.getCurrentUrl(); if (currentUrl.includes('app/kibana')) { From 02ba5fcb131dc82c4d253a8e49e40d3d9e600037 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 29 Apr 2020 12:19:31 -0700 Subject: [PATCH 010/122] [Reporting/Test] Convert functional test code to Typescript (#64601) * Convert the tests to typescript * remove outdated comment * fix typescript * fix "as any" Co-authored-by: Elastic Machine --- .../reporting/{index.js => index.ts} | 9 ++--- .../lib/{compare_pngs.js => compare_pngs.ts} | 13 ++++--- .../discover/{reporting.js => reporting.ts} | 3 +- .../visualize/{reporting.js => reporting.ts} | 3 +- x-pack/test/functional/page_objects/index.ts | 1 - .../{reporting_page.js => reporting_page.ts} | 35 ++++++++----------- 6 files changed, 33 insertions(+), 31 deletions(-) rename x-pack/test/functional/apps/dashboard/reporting/{index.js => index.ts} (94%) rename x-pack/test/functional/apps/dashboard/reporting/lib/{compare_pngs.js => compare_pngs.ts} (90%) rename x-pack/test/functional/apps/discover/{reporting.js => reporting.ts} (96%) rename x-pack/test/functional/apps/visualize/{reporting.js => reporting.ts} (94%) rename x-pack/test/functional/page_objects/{reporting_page.js => reporting_page.ts} (84%) diff --git a/x-pack/test/functional/apps/dashboard/reporting/index.js b/x-pack/test/functional/apps/dashboard/reporting/index.ts similarity index 94% rename from x-pack/test/functional/apps/dashboard/reporting/index.js rename to x-pack/test/functional/apps/dashboard/reporting/index.ts index 99be084d80d74..796e15b4e270f 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/index.js +++ b/x-pack/test/functional/apps/dashboard/reporting/index.ts @@ -5,9 +5,10 @@ */ import expect from '@kbn/expect'; -import path from 'path'; import fs from 'fs'; +import path from 'path'; import { promisify } from 'util'; +import { FtrProviderContext } from '../../../ftr_provider_context'; import { checkIfPngsMatch } from './lib/compare_pngs'; const writeFileAsync = promisify(fs.writeFile); @@ -15,7 +16,7 @@ const mkdirAsync = promisify(fs.mkdir); const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); -export default function({ getService, getPageObjects }) { +export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); const log = getService('log'); @@ -85,14 +86,14 @@ export default function({ getService, getPageObjects }) { describe('Preserve Layout', () => { it('matches baseline report', async function() { - const writeSessionReport = async (name, rawPdf, reportExt) => { + const writeSessionReport = async (name: string, rawPdf: Buffer, reportExt: string) => { const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session'); await mkdirAsync(sessionDirectory, { recursive: true }); const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`); await writeFileAsync(sessionReportPath, rawPdf); return sessionReportPath; }; - const getBaselineReportPath = (fileName, reportExt) => { + const getBaselineReportPath = (fileName: string, reportExt: string) => { const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); log.debug(`getBaselineReportPath (${fullPath})`); diff --git a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts similarity index 90% rename from x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js rename to x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts index 13c97a7fce785..b2eb645c8372c 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.js +++ b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import path from 'path'; -import fs from 'fs'; import { promisify } from 'bluebird'; +import fs from 'fs'; +import path from 'path'; import { comparePngs } from '../../../../../../../test/functional/services/lib/compare_pngs'; -const mkdirAsync = promisify(fs.mkdir); +const mkdirAsync = promisify(fs.mkdir); -export async function checkIfPngsMatch(actualpngPath, baselinepngPath, screenshotsDirectory, log) { +export async function checkIfPngsMatch( + actualpngPath: string, + baselinepngPath: string, + screenshotsDirectory: string, + log: any +) { log.debug(`checkIfpngsMatch: ${actualpngPath} vs ${baselinepngPath}`); // Copy the pngs into the screenshot session directory, as that's where the generated pngs will automatically be // stored. diff --git a/x-pack/test/functional/apps/discover/reporting.js b/x-pack/test/functional/apps/discover/reporting.ts similarity index 96% rename from x-pack/test/functional/apps/discover/reporting.js rename to x-pack/test/functional/apps/discover/reporting.ts index 4aa005fc2db55..7a33e7f5135d4 100644 --- a/x-pack/test/functional/apps/discover/reporting.js +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -5,8 +5,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function({ getService, getPageObjects }) { +export default function({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); diff --git a/x-pack/test/functional/apps/visualize/reporting.js b/x-pack/test/functional/apps/visualize/reporting.ts similarity index 94% rename from x-pack/test/functional/apps/visualize/reporting.js rename to x-pack/test/functional/apps/visualize/reporting.ts index bc252e1ad4134..5ef954e334d81 100644 --- a/x-pack/test/functional/apps/visualize/reporting.js +++ b/x-pack/test/functional/apps/visualize/reporting.ts @@ -5,8 +5,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function({ getService, getPageObjects }) { +export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const browser = getService('browser'); const log = getService('log'); diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 782d57adea770..4b8c2944ef190 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -19,7 +19,6 @@ import { GraphPageProvider } from './graph_page'; import { GrokDebuggerPageProvider } from './grok_debugger_page'; // @ts-ignore not ts yet import { WatcherPageProvider } from './watcher_page'; -// @ts-ignore not ts yet import { ReportingPageProvider } from './reporting_page'; // @ts-ignore not ts yet import { AccountSettingProvider } from './accountsetting_page'; diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.ts similarity index 84% rename from x-pack/test/functional/page_objects/reporting_page.js rename to x-pack/test/functional/page_objects/reporting_page.ts index b24ba8cf95d1c..2c20519a8d214 100644 --- a/x-pack/test/functional/page_objects/reporting_page.js +++ b/x-pack/test/functional/page_objects/reporting_page.ts @@ -4,25 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import http, { IncomingMessage } from 'http'; +import { FtrProviderContext } from 'test/functional/ftr_provider_context'; import { parse } from 'url'; -import http from 'http'; -/* - * NOTE: Reporting is a service, not an app. The page objects that are - * important for generating reports belong to the apps that integrate with the - * Reporting service. Eventually, this file should be dissolved across the - * apps that need it for testing their integration. - * Issue: https://github.com/elastic/kibana/issues/52927 - */ -export function ReportingPageProvider({ getService, getPageObjects }) { +export function ReportingPageProvider({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); - const PageObjects = getPageObjects(['common', 'security', 'share', 'timePicker']); + const PageObjects = getPageObjects(['common', 'security' as any, 'share', 'timePicker']); // FIXME: Security PageObject is not Typescript class ReportingPage { - async forceSharedItemsContainerSize({ width }) { + async forceSharedItemsContainerSize({ width }: { width: number }) { await browser.execute(` var el = document.querySelector('[data-shared-items-container]'); el.style.flex="none"; @@ -30,7 +24,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) { `); } - async getReportURL(timeout) { + async getReportURL(timeout: number) { log.debug('getReportURL'); const url = await testSubjects.getAttribute('downloadCompletedReportButton', 'href', timeout); @@ -48,7 +42,7 @@ export function ReportingPageProvider({ getService, getPageObjects }) { `); } - getResponse(url) { + getResponse(url: string): Promise { log.debug(`getResponse for ${url}`); const auth = 'test_user:changeme'; // FIXME not sure why there is no config that can be read for this const headers = { @@ -62,29 +56,30 @@ export function ReportingPageProvider({ getService, getPageObjects }) { hostname: parsedUrl.hostname, path: parsedUrl.path, port: parsedUrl.port, - responseType: 'arraybuffer', headers, }, - res => { + (res: IncomingMessage) => { resolve(res); } ) - .on('error', e => { + .on('error', (e: Error) => { log.error(e); reject(e); }); }); } - async getRawPdfReportData(url) { - const data = []; // List of Buffer objects + async getRawPdfReportData(url: string): Promise { + const data: Buffer[] = []; // List of Buffer objects log.debug(`getRawPdfReportData for ${url}`); return new Promise(async (resolve, reject) => { const response = await this.getResponse(url).catch(reject); - response.on('data', chunk => data.push(chunk)); - response.on('end', () => resolve(Buffer.concat(data))); + if (response) { + response.on('data', (chunk: Buffer) => data.push(chunk)); + response.on('end', () => resolve(Buffer.concat(data))); + } }); } From d6f0c785d916660b9347f25bb6e3034989f8432a Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 29 Apr 2020 13:27:55 -0600 Subject: [PATCH 011/122] [Maps] do not display EMS or kibana layer wizards when not configured (#64554) * [Maps] do not display EMS or kibana layer wizards when not configured * default tilemap to empty object instead of array * fix typo Co-authored-by: Elastic Machine --- x-pack/legacy/plugins/maps/index.js | 2 +- .../plugins/maps/public/layers/layer_wizard_registry.ts | 5 ++++- .../ems_file_source/ems_boundaries_layer_wizard.tsx | 5 +++++ .../sources/ems_tms_source/ems_base_map_layer_wizard.tsx | 5 +++++ .../kibana_regionmap_layer_wizard.tsx | 6 ++++++ .../kibana_base_map_layer_wizard.tsx | 6 ++++++ x-pack/plugins/maps/public/meta.js | 9 ++++++--- 7 files changed, 33 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index d1e8892fa2c98..a1186e04ee27a 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -54,7 +54,7 @@ export function maps(kibana) { emsLandingPageUrl: mapConfig.emsLandingPageUrl, kbnPkgVersion: serverConfig.get('pkg.version'), regionmapLayers: _.get(mapConfig, 'regionmap.layers', []), - tilemap: _.get(mapConfig, 'tilemap', []), + tilemap: _.get(mapConfig, 'tilemap', {}), }; }, styleSheetPaths: `${__dirname}/public/index.scss`, diff --git a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts index 633e8c86d8c94..7715541b1c52d 100644 --- a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts @@ -20,6 +20,7 @@ export type RenderWizardArguments = { }; export type LayerWizard = { + checkVisibility?: () => boolean; description: string; icon: string; isIndexingSource?: boolean; @@ -34,5 +35,7 @@ export function registerLayerWizard(layerWizard: LayerWizard) { } export function getLayerWizards(): LayerWizard[] { - return [...registry]; + return registry.filter(layerWizard => { + return layerWizard.checkVisibility ? layerWizard.checkVisibility() : true; + }); } diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index f31e770df2d95..a6e2e7f42657c 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -12,8 +12,13 @@ import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry' import { EMSFileCreateSourceEditor } from './create_source_editor'; // @ts-ignore import { EMSFileSource, sourceTitle } from './ems_file_source'; +// @ts-ignore +import { isEmsEnabled } from '../../../meta'; export const emsBoundariesLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + return isEmsEnabled(); + }, description: i18n.translate('xpack.maps.source.emsFileDescription', { defaultMessage: 'Administrative boundaries from Elastic Maps Service', }), diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index ced33a0bcf84a..fc745edbabee8 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -12,8 +12,13 @@ import { EMSTMSSource, sourceTitle } from './ems_tms_source'; import { VectorTileLayer } from '../../vector_tile_layer'; // @ts-ignore import { TileServiceSelect } from './tile_service_select'; +// @ts-ignore +import { isEmsEnabled } from '../../../meta'; export const emsBaseMapLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + return isEmsEnabled(); + }, description: i18n.translate('xpack.maps.source.emsTileDescription', { defaultMessage: 'Tile map service from Elastic Maps Service', }), diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx index 4321501760faf..a9adec2bda2c8 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -12,8 +12,14 @@ import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source'; import { VectorLayer } from '../../vector_layer'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; +// @ts-ignore +import { getKibanaRegionList } from '../../../meta'; export const kibanaRegionMapLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + const regions = getKibanaRegionList(); + return regions.length; + }, description: i18n.translate('xpack.maps.source.kbnRegionMapDescription', { defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml', }), diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx index aeea2d6084f84..141fabeedd3e5 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -12,8 +12,14 @@ import { CreateSourceEditor } from './create_source_editor'; // @ts-ignore import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source'; import { TileLayer } from '../../tile_layer'; +// @ts-ignore +import { getKibanaTileMap } from '../../../meta'; export const kibanaBasemapLayerWizardConfig: LayerWizard = { + checkVisibility: () => { + const tilemap = getKibanaTileMap(); + return !!tilemap.url; + }, description: i18n.translate('xpack.maps.source.kbnTMSDescription', { defaultMessage: 'Tile map service configured in kibana.yml', }), diff --git a/x-pack/plugins/maps/public/meta.js b/x-pack/plugins/maps/public/meta.js index d4612554cf00b..c3245e8e98db2 100644 --- a/x-pack/plugins/maps/public/meta.js +++ b/x-pack/plugins/maps/public/meta.js @@ -36,12 +36,15 @@ function fetchFunction(...args) { return fetch(...args); } +export function isEmsEnabled() { + return getInjectedVarFunc()('isEmsEnabled', true); +} + let emsClient = null; let latestLicenseId = null; export function getEMSClient() { if (!emsClient) { - const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); - if (isEmsEnabled) { + if (isEmsEnabled()) { const proxyElasticMapsServiceInMaps = getInjectedVarFunc()( 'proxyElasticMapsServiceInMaps', false @@ -86,7 +89,7 @@ export function getEMSClient() { } export function getGlyphUrl() { - if (!getInjectedVarFunc()('isEmsEnabled', true)) { + if (!isEmsEnabled()) { return ''; } return getInjectedVarFunc()('proxyElasticMapsServiceInMaps', false) From 2e410d8952324efdc43dd60e5470d4db3bd5257f Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 29 Apr 2020 12:53:06 -0700 Subject: [PATCH 012/122] skip flaky suite (#64812) (#64723) --- .../test_suites/event_log/public_api_integration.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index e5b840b335846..d7bbc29bd861e 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -19,7 +19,9 @@ export default function({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); - describe('Event Log public API', () => { + // FLAKY: https://github.com/elastic/kibana/issues/64723 + // FLAKY: https://github.com/elastic/kibana/issues/64812 + describe.skip('Event Log public API', () => { it('should allow querying for events by Saved Object', async () => { const id = uuid.v4(); From 1669a1044108a11b44b4ee998d55beb84e4e942f Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 29 Apr 2020 15:36:37 -0500 Subject: [PATCH 013/122] [Metrics UI] Fix alerting when a filter query is present (#64575) --- .../metric_threshold_executor.ts | 23 ++++++++++++------- .../apis/infra/metrics_alerting.ts | 18 +++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 946f1c14bf593..cf691f73bdc2c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -51,17 +51,19 @@ const getCurrentValueFromAggregations = ( const getParsedFilterQuery: ( filterQuery: string | undefined -) => Record = filterQuery => { +) => Record | Array> = filterQuery => { if (!filterQuery) return {}; try { return JSON.parse(filterQuery).bool; } catch (e) { - return { - query_string: { - query: filterQuery, - analyze_wildcard: true, + return [ + { + query_string: { + query: filterQuery, + analyze_wildcard: true, + }, }, - }; + ]; } }; @@ -159,8 +161,12 @@ export const getElasticsearchMetricQuery = ( return { query: { bool: { - filter: [...rangeFilters, ...metricFieldFilters], - ...parsedFilterQuery, + filter: [ + ...rangeFilters, + ...metricFieldFilters, + ...(Array.isArray(parsedFilterQuery) ? parsedFilterQuery : []), + ], + ...(!Array.isArray(parsedFilterQuery) ? parsedFilterQuery : {}), }, }, size: 0, @@ -233,6 +239,7 @@ const getMetric: ( body: searchBody, index, }); + return { '*': getCurrentValueFromAggregations(result.aggregations, aggType) }; } catch (e) { return { '*': undefined }; // Trigger an Error state diff --git a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts index 4f17f9db67483..19879f5761ab2 100644 --- a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts +++ b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts @@ -39,6 +39,7 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); } it('should work with a filterQuery', async () => { @@ -53,6 +54,21 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); + }); + it('should work with a filterQuery in KQL format', async () => { + const searchBody = getElasticsearchMetricQuery( + getSearchParams('avg'), + undefined, + '"agent.hostname":"foo"' + ); + const result = await client.search({ + index, + body: searchBody, + }); + expect(result.error).to.not.be.ok(); + expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); }); describe('querying with a groupBy parameter', () => { @@ -65,6 +81,7 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); } it('should work with a filterQuery', async () => { @@ -79,6 +96,7 @@ export default function({ getService }: FtrProviderContext) { }); expect(result.error).to.not.be.ok(); expect(result.hits).to.be.ok(); + expect(result.aggregations).to.be.ok(); }); }); }); From 6338cef317f023e3cfaebcc27a0c0fb1e9b857e8 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 29 Apr 2020 16:41:59 -0400 Subject: [PATCH 014/122] [Ingest] Allow to enable monitoring of elastic agent (#63598) --- .../common/constants/agent_config.ts | 1 + .../common/types/models/agent_config.ts | 9 ++ .../agent_config/components/config_form.tsx | 65 ++++++++- .../list_page/components/create_config.tsx | 1 + .../ingest_manager/server/saved_objects.ts | 1 + .../server/services/agent_config.test.ts | 134 ++++++++++++++++++ .../server/services/agent_config.ts | 45 ++++-- .../server/types/models/agent_config.ts | 3 + 8 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/agent_config.test.ts diff --git a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts index c5067480fb953..9bc1293799d3c 100644 --- a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts @@ -14,6 +14,7 @@ export const DEFAULT_AGENT_CONFIG = { status: AgentConfigStatus.Active, datasources: [], is_default: true, + monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, }; export const DEFAULT_AGENT_CONFIGS_PACKAGES = [DefaultPackages.system]; diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts index 2372caee512af..7705956590c16 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts @@ -23,6 +23,7 @@ export interface NewAgentConfig { namespace?: string; description?: string; is_default?: boolean; + monitoring_enabled?: Array<'logs' | 'metrics'>; } export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes { @@ -60,4 +61,12 @@ export interface FullAgentConfig { }; datasources: FullAgentConfigDatasource[]; revision?: number; + settings?: { + monitoring: { + use_output?: string; + enabled: boolean; + metrics: boolean; + logs: boolean; + }; + }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx index 0d53ca34a1fef..92c44d86e47c6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx @@ -18,6 +18,7 @@ import { EuiText, EuiComboBox, EuiIconTip, + EuiCheckboxGroup, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -30,7 +31,7 @@ interface ValidationResults { const StyledEuiAccordion = styled(EuiAccordion)` .ingest-active-button { - color: ${props => props.theme.eui.euiColorPrimary}}; + color: ${props => props.theme.eui.euiColorPrimary}; } `; @@ -244,6 +245,68 @@ export const AgentConfigForm: React.FunctionComponent = ({ )} + + + + +

+ +

+
+ + + + +
+ + { + acc[key] = true; + return acc; + }, + { logs: false, metrics: false } + )} + onChange={id => { + if (id !== 'logs' && id !== 'metrics') { + return; + } + + const hasLogs = + agentConfig.monitoring_enabled && agentConfig.monitoring_enabled.indexOf(id) >= 0; + + const previousValues = agentConfig.monitoring_enabled || []; + updateAgentConfig({ + monitoring_enabled: hasLogs + ? previousValues.filter(type => type !== id) + : [...previousValues, id], + }); + }} + /> + +
); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx index 1fe116ef36090..9f582e7e2fbe6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx @@ -34,6 +34,7 @@ export const CreateAgentConfigFlyout: React.FunctionComponent = ({ onClos description: '', namespace: '', is_default: undefined, + monitoring_enabled: ['logs', 'metrics'], }); const [isLoading, setIsLoading] = useState(false); const [withSysMonitoring, setWithSysMonitoring] = useState(true); diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index 882258e859555..90fe68e61bb1b 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -128,6 +128,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { updated_on: { type: 'keyword' }, updated_by: { type: 'keyword' }, revision: { type: 'integer' }, + monitoring_enabled: { type: 'keyword' }, }, }, }, diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts new file mode 100644 index 0000000000000..17758f6e3d7f1 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { savedObjectsClientMock } from 'src/core/server/mocks'; +import { agentConfigService } from './agent_config'; +import { Output } from '../types'; + +function getSavedObjectMock(configAttributes: any) { + const mock = savedObjectsClientMock.create(); + + mock.get.mockImplementation(async (type: string, id: string) => { + return { + type, + id, + references: [], + attributes: configAttributes, + }; + }); + + return mock; +} + +jest.mock('./output', () => { + return { + outputService: { + getDefaultOutputId: () => 'test-id', + get: (): Output => { + return { + id: 'test-id', + is_default: true, + name: 'default', + // @ts-ignore + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + }; + }, + }, + }; +}); + +describe('agent config', () => { + describe('getFullConfig', () => { + it('should return a config without monitoring if not monitoring is not enabled', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + }); + const config = await agentConfigService.getFullConfig(soClient, 'config'); + + expect(config).toMatchObject({ + id: 'config', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + datasources: [], + revision: 1, + settings: { + monitoring: { + enabled: false, + logs: false, + metrics: false, + }, + }, + }); + }); + + it('should return a config with monitoring if monitoring is enabled for logs', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + monitoring_enabled: ['logs'], + }); + const config = await agentConfigService.getFullConfig(soClient, 'config'); + + expect(config).toMatchObject({ + id: 'config', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + datasources: [], + revision: 1, + settings: { + monitoring: { + use_output: 'default', + enabled: true, + logs: true, + metrics: false, + }, + }, + }); + }); + + it('should return a config with monitoring if monitoring is enabled for metrics', async () => { + const soClient = getSavedObjectMock({ + revision: 1, + monitoring_enabled: ['metrics'], + }); + const config = await agentConfigService.getFullConfig(soClient, 'config'); + + expect(config).toMatchObject({ + id: 'config', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + ca_sha256: undefined, + api_key: undefined, + }, + }, + datasources: [], + revision: 1, + settings: { + monitoring: { + use_output: 'default', + enabled: true, + logs: false, + metrics: true, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index 75bbfc21293c2..7ab6ef1920c18 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -301,28 +301,49 @@ class AgentConfigService { if (!config) { return null; } + const defaultOutput = await outputService.get( + soClient, + await outputService.getDefaultOutputId(soClient) + ); const agentConfig: FullAgentConfig = { id: config.id, outputs: { // TEMPORARY as we only support a default output - ...[ - await outputService.get(soClient, await outputService.getDefaultOutputId(soClient)), - ].reduce((outputs, { config: outputConfig, name, type, hosts, ca_sha256, api_key }) => { - outputs[name] = { - type, - hosts, - ca_sha256, - api_key, - ...outputConfig, - }; - return outputs; - }, {} as FullAgentConfig['outputs']), + ...[defaultOutput].reduce( + (outputs, { config: outputConfig, name, type, hosts, ca_sha256, api_key }) => { + outputs[name] = { + type, + hosts, + ca_sha256, + api_key, + ...outputConfig, + }; + return outputs; + }, + {} as FullAgentConfig['outputs'] + ), }, datasources: (config.datasources as Datasource[]) .filter(datasource => datasource.enabled) .map(ds => storedDatasourceToAgentDatasource(ds)), revision: config.revision, + ...(config.monitoring_enabled && config.monitoring_enabled.length > 0 + ? { + settings: { + monitoring: { + use_output: defaultOutput.name, + enabled: true, + logs: config.monitoring_enabled.indexOf('logs') >= 0, + metrics: config.monitoring_enabled.indexOf('metrics') >= 0, + }, + }, + } + : { + settings: { + monitoring: { enabled: false, logs: false, metrics: false }, + }, + }), }; return agentConfig; diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts index 040b2eb16289a..59cadf3bd7f74 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts @@ -11,6 +11,9 @@ const AgentConfigBaseSchema = { name: schema.string(), namespace: schema.maybe(schema.string()), description: schema.maybe(schema.string()), + monitoring_enabled: schema.maybe( + schema.arrayOf(schema.oneOf([schema.literal('logs'), schema.literal('metrics')])) + ), }; export const NewAgentConfigSchema = schema.object({ From 110648258c6635e3bc936c389e06533b00960981 Mon Sep 17 00:00:00 2001 From: Henry Harding Date: Wed, 29 Apr 2020 17:18:51 -0400 Subject: [PATCH 015/122] Improve alpha messaging (#64692) * use fixed table layout * add alpha messaging flyout * Added alpha badge + data streams link * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx Co-Authored-By: Jen Huang * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx Co-Authored-By: Jen Huang * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx Co-Authored-By: Jen Huang * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx Co-Authored-By: Jen Huang * remove small tags * change messaging from alpha to experimental * add period * remove unused imports * fixed i18n ids Co-authored-by: Jen Huang Co-authored-by: Elastic Machine --- .../components/alpha_flyout.tsx | 101 ++++++++++++++++++ .../components/alpha_messaging.tsx | 50 +++++---- .../sections/overview/index.tsx | 24 ++++- 3 files changed, 153 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx new file mode 100644 index 0000000000000..1e7a14e350229 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx @@ -0,0 +1,101 @@ +/* + * 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 { + EuiButtonEmpty, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiLink, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + onClose: () => void; +} + +export const AlphaFlyout: React.FunctionComponent = ({ onClose }) => { + return ( + + + +

+ +

+
+
+ + +

+ +

+ + + + ), + forumLink: ( + + + + ), + }} + /> +

+ +

+ + + + ), + }} + /> +

+
+
+ + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx index 0f3ddee29fa44..5a06a9a879441 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx @@ -3,35 +3,45 @@ * 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 React, { useState } from 'react'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiText } from '@elastic/eui'; +import { EuiText, EuiLink } from '@elastic/eui'; +import { AlphaFlyout } from './alpha_flyout'; const Message = styled(EuiText).attrs(props => ({ color: 'subdued', textAlign: 'center', + size: 's', }))` padding: ${props => props.theme.eui.paddingSizes.m}; `; -export const AlphaMessaging: React.FC<{}> = () => ( - -

- - +export const AlphaMessaging: React.FC<{}> = () => { + const [isAlphaFlyoutOpen, setIsAlphaFlyoutOpen] = useState(false); + + return ( + <> + +

+ + + + {' – '} - - {' – '} - - -

-
-); + />{' '} + setIsAlphaFlyoutOpen(true)}> + View more details. + +

+ + {isAlphaFlyoutOpen && setIsAlphaFlyoutOpen(false)} />} + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx index 05d150fd9ae23..70d8e7d6882f8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx @@ -8,6 +8,7 @@ import styled from 'styled-components'; import { EuiButton, EuiButtonEmpty, + EuiBetaBadge, EuiPanel, EuiText, EuiTitle, @@ -19,10 +20,11 @@ import { EuiFlexItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { WithHeaderLayout } from '../../layouts'; import { useLink, useGetAgentConfigs } from '../../hooks'; import { AgentEnrollmentFlyout } from '../fleet/agent_list_page/components'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../../constants'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../../constants'; const OverviewPanel = styled(EuiPanel).attrs(props => ({ paddingSize: 'm', @@ -57,6 +59,11 @@ const OverviewStats = styled(EuiDescriptionList).attrs(props => ({ } `; +const AlphaBadge = styled(EuiBetaBadge)` + vertical-align: top; + margin-left: ${props => props.theme.eui.paddingSizes.s}; +`; + export const IngestManagerOverview: React.FunctionComponent = () => { // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); @@ -79,6 +86,19 @@ export const IngestManagerOverview: React.FunctionComponent = () => { id="xpack.ingestManager.overviewPageTitle" defaultMessage="Ingest Manager" /> + @@ -213,7 +233,7 @@ export const IngestManagerOverview: React.FunctionComponent = () => { /> - + Date: Wed, 29 Apr 2020 19:14:47 -0400 Subject: [PATCH 016/122] [Lens] Use suggestion system in chart switcher for subtypes (#64613) Co-authored-by: Elastic Machine --- .../datatable_visualization/visualization.tsx | 4 ++ .../config_panel/chart_switch.test.tsx | 33 +++++++++--- .../config_panel/chart_switch.tsx | 25 ++++++--- .../editor_frame/suggestion_helpers.ts | 9 +++- .../public/editor_frame_service/mocks.tsx | 1 + .../metric_visualization.tsx | 4 ++ x-pack/plugins/lens/public/types.ts | 9 ++++ .../xy_visualization/xy_visualization.test.ts | 41 +++++++++++++- .../xy_visualization/xy_visualization.tsx | 53 ++++++++++++------- 9 files changed, 143 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 359c06a6a9ebc..48729448b2ea5 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -41,6 +41,10 @@ export const datatableVisualization: Visualization< }, ], + getVisualizationTypeId() { + return 'lnsDatatable'; + }, + getLayerIds(state) { return state.layers.map(l => l.layerId); }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx index 3c61d270b1bcf..c8d8064e60e38 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx @@ -62,7 +62,25 @@ describe('chart_switch', () => { id: 'subvisC2', label: 'C2', }, + { + icon: 'empty', + id: 'subvisC3', + label: 'C3', + }, ], + getSuggestions: jest.fn(options => { + if (options.subVisualizationId === 'subvisC2') { + return []; + } + return [ + { + score: 1, + title: '', + state: `suggestion`, + previewIcon: 'empty', + }, + ]; + }), }, }; } @@ -313,10 +331,11 @@ describe('chart_switch', () => { expect(getMenuItem('subvisB', component).prop('betaBadgeIconType')).toBeUndefined(); }); - it('should not indicate data loss if visualization is not changed', () => { + it('should not show a warning when the subvisualization is the same', () => { const dispatch = jest.fn(); const frame = mockFrame(['a', 'b', 'c']); const visualizations = mockVisualizations(); + visualizations.visC.getVisualizationTypeId.mockReturnValue('subvisC2'); const switchVisualizationType = jest.fn(() => 'therebedragons'); visualizations.visC.switchVisualizationType = switchVisualizationType; @@ -333,10 +352,10 @@ describe('chart_switch', () => { /> ); - expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).toBeUndefined(); + expect(getMenuItem('subvisC2', component).prop('betaBadgeIconType')).not.toBeDefined(); }); - it('should remove all layers if there is no suggestion', () => { + it('should get suggestions when switching subvisualization', () => { const dispatch = jest.fn(); const visualizations = mockVisualizations(); visualizations.visB.getSuggestions.mockReturnValueOnce([]); @@ -377,7 +396,7 @@ describe('chart_switch', () => { const dispatch = jest.fn(); const frame = mockFrame(['a', 'b', 'c']); const visualizations = mockVisualizations(); - const switchVisualizationType = jest.fn(() => 'therebedragons'); + const switchVisualizationType = jest.fn(() => 'switched'); visualizations.visC.switchVisualizationType = switchVisualizationType; @@ -393,12 +412,12 @@ describe('chart_switch', () => { /> ); - switchTo('subvisC2', component); - expect(switchVisualizationType).toHaveBeenCalledWith('subvisC2', 'therebegriffins'); + switchTo('subvisC3', component); + expect(switchVisualizationType).toHaveBeenCalledWith('subvisC3', 'suggestion'); expect(dispatch).toHaveBeenCalledWith( expect.objectContaining({ type: 'SWITCH_VISUALIZATION', - initialState: 'therebedragons', + initialState: 'switched', }) ); expect(frame.removeLayers).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx index 1461449f3c1c8..d73f83e75c0e4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx @@ -105,7 +105,16 @@ export function ChartSwitch(props: Props) { const switchVisType = props.visualizationMap[visualizationId].switchVisualizationType || ((_type: string, initialState: unknown) => initialState); - if (props.visualizationId === visualizationId) { + const layers = Object.entries(props.framePublicAPI.datasourceLayers); + const containsData = layers.some( + ([_layerId, datasource]) => datasource.getTableSpec().length > 0 + ); + // Always show the active visualization as a valid selection + if ( + props.visualizationId === visualizationId && + props.visualizationState && + newVisualization.getVisualizationTypeId(props.visualizationState) === subVisualizationId + ) { return { visualizationId, subVisualizationId, @@ -116,13 +125,13 @@ export function ChartSwitch(props: Props) { }; } - const layers = Object.entries(props.framePublicAPI.datasourceLayers); - const containsData = layers.some( - ([_layerId, datasource]) => datasource.getTableSpec().length > 0 + const topSuggestion = getTopSuggestion( + props, + visualizationId, + newVisualization, + subVisualizationId ); - const topSuggestion = getTopSuggestion(props, visualizationId, newVisualization); - let dataLoss: VisualizationSelection['dataLoss']; if (!containsData) { @@ -250,7 +259,8 @@ export function ChartSwitch(props: Props) { function getTopSuggestion( props: Props, visualizationId: string, - newVisualization: Visualization + newVisualization: Visualization, + subVisualizationId?: string ): Suggestion | undefined { const suggestions = getSuggestions({ datasourceMap: props.datasourceMap, @@ -258,6 +268,7 @@ function getTopSuggestion( visualizationMap: { [visualizationId]: newVisualization }, activeVisualizationId: props.visualizationId, visualizationState: props.visualizationState, + subVisualizationId, }).filter(suggestion => { // don't use extended versions of current data table on switching between visualizations // to avoid confusing the user. diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index eabcdfa7a24ab..949ae1f43448e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -44,6 +44,7 @@ export function getSuggestions({ datasourceStates, visualizationMap, activeVisualizationId, + subVisualizationId, visualizationState, field, }: { @@ -57,6 +58,7 @@ export function getSuggestions({ >; visualizationMap: Record; activeVisualizationId: string | null; + subVisualizationId?: string; visualizationState: unknown; field?: unknown; }): Suggestion[] { @@ -89,7 +91,8 @@ export function getSuggestions({ table, visualizationId, datasourceSuggestion, - currentVisualizationState + currentVisualizationState, + subVisualizationId ); }) ) @@ -108,13 +111,15 @@ function getVisualizationSuggestions( table: TableSuggestion, visualizationId: string, datasourceSuggestion: DatasourceSuggestion & { datasourceId: string }, - currentVisualizationState: unknown + currentVisualizationState: unknown, + subVisualizationId?: string ) { return visualization .getSuggestions({ table, state: currentVisualizationState, keptLayerIds: datasourceSuggestion.keptLayerIds, + subVisualizationId, }) .map(({ state, ...visualizationSuggestion }) => ({ ...visualizationSuggestion, diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 50cd1ad8bd53a..e684fe8b3b5d6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -28,6 +28,7 @@ export function createMockVisualization(): jest.Mocked { label: 'TEST', }, ], + getVisualizationTypeId: jest.fn(_state => 'empty'), getDescription: jest.fn(_state => ({ label: '' })), switchVisualizationType: jest.fn((_, x) => x), getPersistableState: jest.fn(_state => _state), diff --git a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx index 73b8019a31eaa..04a1c3865f22d 100644 --- a/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx @@ -53,6 +53,10 @@ export const metricVisualization: Visualization = { }, ], + getVisualizationTypeId() { + return 'lnsMetric'; + }, + clearLayer(state) { return { ...state, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 181f192520d0d..ed0af8545f012 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -312,6 +312,10 @@ export interface SuggestionRequest { * The visualization needs to know which table is being suggested */ keptLayerIds: string[]; + /** + * Different suggestions can be generated for each subtype of the visualization + */ + subVisualizationId?: string; } /** @@ -388,6 +392,11 @@ export interface Visualization { * but can register multiple subtypes */ visualizationTypes: VisualizationType[]; + /** + * Return the ID of the current visualization. Used to highlight + * the active subtype of the visualization. + */ + getVisualizationTypeId: (state: T) => string; /** * If the visualization has subtypes, update the subtype in state. */ diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts index beccf0dc46eb4..d176905c65120 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.test.ts @@ -27,7 +27,7 @@ function exampleState(): State { } describe('xy_visualization', () => { - describe('getDescription', () => { + describe('#getDescription', () => { function mixedState(...types: SeriesType[]) { const state = exampleState(); return { @@ -81,6 +81,45 @@ describe('xy_visualization', () => { }); }); + describe('#getVisualizationTypeId', () => { + function mixedState(...types: SeriesType[]) { + const state = exampleState(); + return { + ...state, + layers: types.map((t, i) => ({ + ...state.layers[0], + layerId: `layer_${i}`, + seriesType: t, + })), + }; + } + + it('should show mixed when each layer is different', () => { + expect(xyVisualization.getVisualizationTypeId(mixedState('bar', 'line'))).toEqual('mixed'); + }); + + it('should show the preferredSeriesType if there are no layers', () => { + expect(xyVisualization.getVisualizationTypeId(mixedState())).toEqual('bar'); + }); + + it('should combine multiple layers into one type', () => { + expect( + xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal', 'bar_horizontal')) + ).toEqual('bar_horizontal'); + }); + + it('should return the subtype for single layers', () => { + expect(xyVisualization.getVisualizationTypeId(mixedState('area'))).toEqual('area'); + expect(xyVisualization.getVisualizationTypeId(mixedState('line'))).toEqual('line'); + expect(xyVisualization.getVisualizationTypeId(mixedState('area_stacked'))).toEqual( + 'area_stacked' + ); + expect(xyVisualization.getVisualizationTypeId(mixedState('bar_horizontal_stacked'))).toEqual( + 'bar_horizontal_stacked' + ); + }); + }); + describe('#initialize', () => { it('loads default state', () => { const mockFrame = createMockFramePublicAPI(); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx index c72fa0fec24d7..e91edf9cc0183 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_visualization.tsx @@ -12,7 +12,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu } from './xy_config_panel'; -import { Visualization, OperationMetadata } from '../types'; +import { Visualization, OperationMetadata, VisualizationType } from '../types'; import { State, PersistableState, SeriesType, visualizationTypes, LayerConfig } from './types'; import { toExpression, toPreviewExpression } from './to_expression'; import chartBarStackedSVG from '../assets/chart_bar_stacked.svg'; @@ -24,6 +24,18 @@ const defaultSeriesType = 'bar_stacked'; const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; const isBucketed = (op: OperationMetadata) => op.isBucketed; +function getVisualizationType(state: State): VisualizationType | 'mixed' { + if (!state.layers.length) { + return ( + visualizationTypes.find(t => t.id === state.preferredSeriesType) ?? visualizationTypes[0] + ); + } + const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType); + const seriesTypes = _.unique(state.layers.map(l => l.seriesType)); + + return visualizationType && seriesTypes.length === 1 ? visualizationType : 'mixed'; +} + function getDescription(state?: State) { if (!state) { return { @@ -34,32 +46,31 @@ function getDescription(state?: State) { }; } + const visualizationType = getVisualizationType(state); + if (!state.layers.length) { - const visualizationType = visualizationTypes.find(v => v.id === state.preferredSeriesType)!; + const preferredType = visualizationType as VisualizationType; return { - icon: visualizationType.largeIcon || visualizationType.icon, - label: visualizationType.label, + icon: preferredType.largeIcon || preferredType.icon, + label: preferredType.label, }; } - const visualizationType = visualizationTypes.find(t => t.id === state.layers[0].seriesType)!; - const seriesTypes = _.unique(state.layers.map(l => l.seriesType)); - return { icon: - seriesTypes.length === 1 - ? visualizationType.largeIcon || visualizationType.icon - : chartMixedSVG, + visualizationType === 'mixed' + ? chartMixedSVG + : visualizationType.largeIcon || visualizationType.icon, label: - seriesTypes.length === 1 - ? visualizationType.label - : isHorizontalChart(state.layers) - ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', { - defaultMessage: 'Mixed horizontal bar', - }) - : i18n.translate('xpack.lens.xyVisualization.mixedLabel', { - defaultMessage: 'Mixed XY', - }), + visualizationType === 'mixed' + ? isHorizontalChart(state.layers) + ? i18n.translate('xpack.lens.xyVisualization.mixedBarHorizontalLabel', { + defaultMessage: 'Mixed horizontal bar', + }) + : i18n.translate('xpack.lens.xyVisualization.mixedLabel', { + defaultMessage: 'Mixed XY', + }) + : visualizationType.label, }; } @@ -67,6 +78,10 @@ export const xyVisualization: Visualization = { id: 'lnsXY', visualizationTypes, + getVisualizationTypeId(state) { + const type = getVisualizationType(state); + return type === 'mixed' ? type : type.id; + }, getLayerIds(state) { return state.layers.map(l => l.layerId); From 9b65cbd92bb410b008ed8655937cc796369056e3 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Thu, 30 Apr 2020 02:10:14 +0200 Subject: [PATCH 017/122] [Lens] Bind all time fields to the time picker (#63874) * Bind non primary time fields to timepicker * Fix typescript argument types * Allow auto interval on all fields * Remove lens_auto_date function * Fix existing jest tests and add test todos * Remove lens_auto_date from esarchives * Add TimeBuckets jest tests * Fix typo in esarchiver * Address review feedback * Make code a bit better readable * Fix default time field retrieval * Fix TS errors * Add esaggs interpreter tests * Change public API doc of data plugin * Add toExpression tests for index pattern datasource * Add migration stub * Add full migration * Fix naming inconsistency in esaggs * Fix naming issue * Revert archives to un-migrated version * Ignore expressions that are already migrated * test: remove extra spaces and timeField=\\"products.created_on\\"} to timeField=\"products.created_on\"} * Rename all timeField -> timeFields * Combine duplicate functions * Fix boolean error and add test for it * Commit API changes Co-authored-by: Wylie Conlon Co-authored-by: Elastic Machine Co-authored-by: Marta Bondyra --- ...bana-plugin-plugins-data-public.gettime.md | 7 +- ...-data-public.iindexpattern.gettimefield.md | 15 +++ ...lugin-plugins-data-public.iindexpattern.md | 6 + .../kibana-plugin-plugins-data-public.md | 2 +- .../data/common/index_patterns/types.ts | 1 + src/plugins/data/public/public.api.md | 7 +- .../public/query/timefilter/get_time.test.ts | 38 ++++++ .../data/public/query/timefilter/get_time.ts | 27 ++-- .../data/public/query/timefilter/index.ts | 2 +- .../public/query/timefilter/timefilter.ts | 4 +- .../search/aggs/buckets/date_histogram.ts | 2 +- .../data/public/search/expressions/esaggs.ts | 43 +++++- .../data/public/search/tabify/buckets.test.ts | 63 +++++++-- .../data/public/search/tabify/buckets.ts | 12 +- .../data/public/search/tabify/tabify.ts | 18 +-- .../data/public/search/tabify/types.ts | 10 +- src/plugins/data/server/server.api.md | 2 + .../test_suites/run_pipeline/esaggs.ts | 93 +++++++++++++ .../test_suites/run_pipeline/index.ts | 1 + .../indexpattern_datasource/auto_date.test.ts | 83 ------------ .../indexpattern_datasource/auto_date.ts | 79 ------------ .../public/indexpattern_datasource/index.ts | 4 +- .../indexpattern.test.ts | 102 +++++++++++++-- .../indexpattern_datasource/to_expression.ts | 23 ++-- x-pack/plugins/lens/server/migrations.test.ts | 120 +++++++++++++++++ x-pack/plugins/lens/server/migrations.ts | 122 +++++++++++++++++- 26 files changed, 635 insertions(+), 251 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md create mode 100644 test/interpreter_functional/test_suites/run_pipeline/esaggs.ts delete mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts delete mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md index 04a0d871cab2d..3969a97fa7789 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.gettime.md @@ -7,7 +7,10 @@ Signature: ```typescript -export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined; +export declare function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: { + forceNow?: Date; + fieldName?: string; +}): import("../..").RangeFilter | undefined; ``` ## Parameters @@ -16,7 +19,7 @@ export declare function getTime(indexPattern: IIndexPattern | undefined, timeRan | --- | --- | --- | | indexPattern | IIndexPattern | undefined | | | timeRange | TimeRange | | -| forceNow | Date | | +| options | {
forceNow?: Date;
fieldName?: string;
} | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md new file mode 100644 index 0000000000000..c3998876c9712 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [getTimeField](./kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md) + +## IIndexPattern.getTimeField() method + +Signature: + +```typescript +getTimeField?(): IFieldType | undefined; +``` +Returns: + +`IFieldType | undefined` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index 1bbd6cf67f0ce..1cb89822eb605 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -21,3 +21,9 @@ export interface IIndexPattern | [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | string | | | [type](./kibana-plugin-plugins-data-public.iindexpattern.type.md) | string | | +## Methods + +| Method | Description | +| --- | --- | +| [getTimeField()](./kibana-plugin-plugins-data-public.iindexpattern.gettimefield.md) | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 0fd82ffb2240c..e1df493143b73 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -43,7 +43,7 @@ | [getEsPreference(uiSettings, sessionId)](./kibana-plugin-plugins-data-public.getespreference.md) | | | [getQueryLog(uiSettings, storage, appName, language)](./kibana-plugin-plugins-data-public.getquerylog.md) | | | [getSearchErrorType({ message })](./kibana-plugin-plugins-data-public.getsearcherrortype.md) | | -| [getTime(indexPattern, timeRange, forceNow)](./kibana-plugin-plugins-data-public.gettime.md) | | +| [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-public.gettime.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-public.plugin.md) | | ## Interfaces diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 698edbf9cd6a8..e21d27a70e02a 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -26,6 +26,7 @@ export interface IIndexPattern { id?: string; type?: string; timeFieldName?: string; + getTimeField?(): IFieldType | undefined; fieldFormatMap?: Record< string, { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 86560b3ccf7b1..91dea66f06a94 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -699,7 +699,10 @@ export function getSearchErrorType({ message }: Pick): " // Warning: (ae-missing-release-tag) "getTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined; +export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, options?: { + forceNow?: Date; + fieldName?: string; +}): import("../..").RangeFilter | undefined; // Warning: (ae-missing-release-tag) "IAggConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -842,6 +845,8 @@ export interface IIndexPattern { // (undocumented) fields: IFieldType[]; // (undocumented) + getTimeField?(): IFieldType | undefined; + // (undocumented) id?: string; // (undocumented) timeFieldName?: string; diff --git a/src/plugins/data/public/query/timefilter/get_time.test.ts b/src/plugins/data/public/query/timefilter/get_time.test.ts index a8eb3a3fe8102..4dba157a6f554 100644 --- a/src/plugins/data/public/query/timefilter/get_time.test.ts +++ b/src/plugins/data/public/query/timefilter/get_time.test.ts @@ -51,5 +51,43 @@ describe('get_time', () => { }); clock.restore(); }); + + test('build range filter for non-primary field', () => { + const clock = sinon.useFakeTimers(moment.utc([2000, 1, 1, 0, 0, 0, 0]).valueOf()); + + const filter = getTime( + { + id: 'test', + title: 'test', + timeFieldName: 'date', + fields: [ + { + name: 'date', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + { + name: 'myCustomDate', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + ], + } as any, + { from: 'now-60y', to: 'now' }, + { fieldName: 'myCustomDate' } + ); + expect(filter!.range.myCustomDate).toEqual({ + gte: '1940-02-01T00:00:00.000Z', + lte: '2000-02-01T00:00:00.000Z', + format: 'strict_date_optional_time', + }); + clock.restore(); + }); }); }); diff --git a/src/plugins/data/public/query/timefilter/get_time.ts b/src/plugins/data/public/query/timefilter/get_time.ts index fa15406189041..9cdd25d3213ce 100644 --- a/src/plugins/data/public/query/timefilter/get_time.ts +++ b/src/plugins/data/public/query/timefilter/get_time.ts @@ -19,7 +19,7 @@ import dateMath from '@elastic/datemath'; import { IIndexPattern } from '../..'; -import { TimeRange, IFieldType, buildRangeFilter } from '../../../common'; +import { TimeRange, buildRangeFilter } from '../../../common'; interface CalculateBoundsOptions { forceNow?: Date; @@ -35,18 +35,27 @@ export function calculateBounds(timeRange: TimeRange, options: CalculateBoundsOp export function getTime( indexPattern: IIndexPattern | undefined, timeRange: TimeRange, + options?: { forceNow?: Date; fieldName?: string } +) { + return createTimeRangeFilter( + indexPattern, + timeRange, + options?.fieldName || indexPattern?.timeFieldName, + options?.forceNow + ); +} + +function createTimeRangeFilter( + indexPattern: IIndexPattern | undefined, + timeRange: TimeRange, + fieldName?: string, forceNow?: Date ) { if (!indexPattern) { - // in CI, we sometimes seem to fail here. return; } - - const timefield: IFieldType | undefined = indexPattern.fields.find( - field => field.name === indexPattern.timeFieldName - ); - - if (!timefield) { + const field = indexPattern.fields.find(f => f.name === (fieldName || indexPattern.timeFieldName)); + if (!field) { return; } @@ -55,7 +64,7 @@ export function getTime( return; } return buildRangeFilter( - timefield, + field, { ...(bounds.min && { gte: bounds.min.toISOString() }), ...(bounds.max && { lte: bounds.max.toISOString() }), diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts index a6260e782c12f..034af03842ab8 100644 --- a/src/plugins/data/public/query/timefilter/index.ts +++ b/src/plugins/data/public/query/timefilter/index.ts @@ -22,6 +22,6 @@ export { TimefilterService, TimefilterSetup } from './timefilter_service'; export * from './types'; export { Timefilter, TimefilterContract } from './timefilter'; export { TimeHistory, TimeHistoryContract } from './time_history'; -export { getTime } from './get_time'; +export { getTime, calculateBounds } from './get_time'; export { changeTimeFilter } from './lib/change_time_filter'; export { extractTimeFilter } from './lib/extract_time_filter'; diff --git a/src/plugins/data/public/query/timefilter/timefilter.ts b/src/plugins/data/public/query/timefilter/timefilter.ts index 4fbdac47fb3b0..86ef69be572a9 100644 --- a/src/plugins/data/public/query/timefilter/timefilter.ts +++ b/src/plugins/data/public/query/timefilter/timefilter.ts @@ -164,7 +164,9 @@ export class Timefilter { }; public createFilter = (indexPattern: IndexPattern, timeRange?: TimeRange) => { - return getTime(indexPattern, timeRange ? timeRange : this._time, this.getForceNow()); + return getTime(indexPattern, timeRange ? timeRange : this._time, { + forceNow: this.getForceNow(), + }); }; public getBounds(): TimeRangeBounds { diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts index 57f3aa85ad944..3ecdc17cb57f3 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -45,7 +45,7 @@ const updateTimeBuckets = ( customBuckets?: IBucketDateHistogramAggConfig['buckets'] ) => { const bounds = - agg.params.timeRange && agg.fieldIsTimeField() + agg.params.timeRange && (agg.fieldIsTimeField() || agg.params.interval === 'auto') ? timefilter.calculateBounds(agg.params.timeRange) : undefined; const buckets = customBuckets || agg.buckets; diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 087b83127079f..eec75b0841133 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -32,8 +32,15 @@ import { Adapters } from '../../../../../plugins/inspector/public'; import { IAggConfigs } from '../aggs'; import { ISearchSource } from '../search_source'; import { tabifyAggResponse } from '../tabify'; -import { Filter, Query, serializeFieldFormat, TimeRange } from '../../../common'; -import { FilterManager, getTime } from '../../query'; +import { + Filter, + Query, + serializeFieldFormat, + TimeRange, + IIndexPattern, + isRangeFilter, +} from '../../../common'; +import { FilterManager, calculateBounds, getTime } from '../../query'; import { getSearchService, getQueryService, getIndexPatterns } from '../../services'; import { buildTabularInspectorData } from './build_tabular_inspector_data'; import { getRequestInspectorStats, getResponseInspectorStats, serializeAggConfig } from './utils'; @@ -42,6 +49,8 @@ export interface RequestHandlerParams { searchSource: ISearchSource; aggs: IAggConfigs; timeRange?: TimeRange; + timeFields?: string[]; + indexPattern?: IIndexPattern; query?: Query; filters?: Filter[]; forceFetch: boolean; @@ -65,12 +74,15 @@ interface Arguments { partialRows: boolean; includeFormatHints: boolean; aggConfigs: string; + timeFields?: string[]; } const handleCourierRequest = async ({ searchSource, aggs, timeRange, + timeFields, + indexPattern, query, filters, forceFetch, @@ -111,9 +123,19 @@ const handleCourierRequest = async ({ return aggs.onSearchRequestStart(paramSearchSource, options); }); - if (timeRange) { + // If timeFields have been specified, use the specified ones, otherwise use primary time field of index + // pattern if it's available. + const defaultTimeField = indexPattern?.getTimeField?.(); + const defaultTimeFields = defaultTimeField ? [defaultTimeField.name] : []; + const allTimeFields = timeFields && timeFields.length > 0 ? timeFields : defaultTimeFields; + + // If a timeRange has been specified and we had at least one timeField available, create range + // filters for that those time fields + if (timeRange && allTimeFields.length > 0) { timeFilterSearchSource.setField('filter', () => { - return getTime(searchSource.getField('index'), timeRange); + return allTimeFields + .map(fieldName => getTime(indexPattern, timeRange, { fieldName })) + .filter(isRangeFilter); }); } @@ -181,11 +203,13 @@ const handleCourierRequest = async ({ (searchSource as any).finalResponse = resp; - const parsedTimeRange = timeRange ? getTime(aggs.indexPattern, timeRange) : null; + const parsedTimeRange = timeRange ? calculateBounds(timeRange) : null; const tabifyParams = { metricsAtAllLevels, partialRows, - timeRange: parsedTimeRange ? parsedTimeRange.range : undefined, + timeRange: parsedTimeRange + ? { from: parsedTimeRange.min, to: parsedTimeRange.max, timeFields: allTimeFields } + : undefined, }; const tabifyCacheHash = calculateObjectHash({ tabifyAggs: aggs, ...tabifyParams }); @@ -242,6 +266,11 @@ export const esaggs = (): ExpressionFunctionDefinition { const check = (aggResp: any, count: number, keys: string[]) => { @@ -187,9 +192,9 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 150, - lte: 350, - name: 'date', + from: moment(150), + to: moment(350), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); @@ -204,9 +209,9 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 150, - lte: 350, - name: 'date', + from: moment(150), + to: moment(350), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); @@ -221,9 +226,9 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 100, - lte: 400, - name: 'date', + from: moment(100), + to: moment(400), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); @@ -238,13 +243,47 @@ describe('Buckets wrapper', () => { }, }; const timeRange = { - gte: 150, - lte: 350, - name: 'date', + from: moment(150), + to: moment(350), + timeFields: ['date'], }; const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); expect(buckets).toHaveLength(4); }); + + test('does drop bucket when multiple time fields specified', () => { + const aggParams = { + drop_partials: true, + field: { + name: 'date', + }, + }; + const timeRange = { + from: moment(100), + to: moment(350), + timeFields: ['date', 'other_datefield'], + }; + const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); + + expect(buckets.buckets.map((b: Bucket) => b.key)).toEqual([100, 200]); + }); + + test('does not drop bucket when no timeFields have been specified', () => { + const aggParams = { + drop_partials: true, + field: { + name: 'date', + }, + }; + const timeRange = { + from: moment(100), + to: moment(350), + timeFields: [], + }; + const buckets = new TabifyBuckets(aggResp, aggParams, timeRange); + + expect(buckets.buckets.map((b: Bucket) => b.key)).toEqual([0, 100, 200, 300]); + }); }); }); diff --git a/src/plugins/data/public/search/tabify/buckets.ts b/src/plugins/data/public/search/tabify/buckets.ts index 971e820ac6ddf..cd52a09caeaad 100644 --- a/src/plugins/data/public/search/tabify/buckets.ts +++ b/src/plugins/data/public/search/tabify/buckets.ts @@ -20,7 +20,7 @@ import { get, isPlainObject, keys, findKey } from 'lodash'; import moment from 'moment'; import { IAggConfig } from '../aggs'; -import { AggResponseBucket, TabbedRangeFilterParams } from './types'; +import { AggResponseBucket, TabbedRangeFilterParams, TimeRangeInformation } from './types'; type AggParams = IAggConfig['params'] & { drop_partials: boolean; @@ -36,7 +36,7 @@ export class TabifyBuckets { buckets: any; _keys: any[] = []; - constructor(aggResp: any, aggParams?: AggParams, timeRange?: TabbedRangeFilterParams) { + constructor(aggResp: any, aggParams?: AggParams, timeRange?: TimeRangeInformation) { if (aggResp && aggResp.buckets) { this.buckets = aggResp.buckets; } else if (aggResp) { @@ -107,12 +107,12 @@ export class TabifyBuckets { // dropPartials should only be called if the aggParam setting is enabled, // and the agg field is the same as the Time Range. - private dropPartials(params: AggParams, timeRange?: TabbedRangeFilterParams) { + private dropPartials(params: AggParams, timeRange?: TimeRangeInformation) { if ( !timeRange || this.buckets.length <= 1 || this.objectMode || - params.field.name !== timeRange.name + !timeRange.timeFields.includes(params.field.name) ) { return; } @@ -120,10 +120,10 @@ export class TabifyBuckets { const interval = this.buckets[1].key - this.buckets[0].key; this.buckets = this.buckets.filter((bucket: AggResponseBucket) => { - if (moment(bucket.key).isBefore(timeRange.gte)) { + if (moment(bucket.key).isBefore(timeRange.from)) { return false; } - if (moment(bucket.key + interval).isAfter(timeRange.lte)) { + if (moment(bucket.key + interval).isAfter(timeRange.to)) { return false; } return true; diff --git a/src/plugins/data/public/search/tabify/tabify.ts b/src/plugins/data/public/search/tabify/tabify.ts index e93e989034252..9cb55f94537c5 100644 --- a/src/plugins/data/public/search/tabify/tabify.ts +++ b/src/plugins/data/public/search/tabify/tabify.ts @@ -20,7 +20,7 @@ import { get } from 'lodash'; import { TabbedAggResponseWriter } from './response_writer'; import { TabifyBuckets } from './buckets'; -import { TabbedResponseWriterOptions, TabbedRangeFilterParams } from './types'; +import { TabbedResponseWriterOptions } from './types'; import { AggResponseBucket } from './types'; import { AggGroupNames, IAggConfigs } from '../aggs'; @@ -54,7 +54,7 @@ export function tabifyAggResponse( switch (agg.type.type) { case AggGroupNames.Buckets: const aggBucket = get(bucket, agg.id); - const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, timeRange); + const tabifyBuckets = new TabifyBuckets(aggBucket, agg.params, respOpts?.timeRange); if (tabifyBuckets.length) { tabifyBuckets.forEach((subBucket, tabifyBucketKey) => { @@ -153,20 +153,6 @@ export function tabifyAggResponse( doc_count: esResponse.hits.total, }; - let timeRange: TabbedRangeFilterParams | undefined; - - // Extract the time range object if provided - if (respOpts && respOpts.timeRange) { - const [timeRangeKey] = Object.keys(respOpts.timeRange); - - if (timeRangeKey) { - timeRange = { - name: timeRangeKey, - ...respOpts.timeRange[timeRangeKey], - }; - } - } - collectBucket(aggConfigs, write, topLevelBucket, '', 1); return write.response(); diff --git a/src/plugins/data/public/search/tabify/types.ts b/src/plugins/data/public/search/tabify/types.ts index 1e051880d3f19..72e91eb58c8a9 100644 --- a/src/plugins/data/public/search/tabify/types.ts +++ b/src/plugins/data/public/search/tabify/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { Moment } from 'moment'; import { RangeFilterParams } from '../../../common'; import { IAggConfig } from '../aggs'; @@ -25,11 +26,18 @@ export interface TabbedRangeFilterParams extends RangeFilterParams { name: string; } +/** @internal */ +export interface TimeRangeInformation { + from?: Moment; + to?: Moment; + timeFields: string[]; +} + /** @internal **/ export interface TabbedResponseWriterOptions { metricsAtAllLevels: boolean; partialRows: boolean; - timeRange?: { [key: string]: RangeFilterParams }; + timeRange?: TimeRangeInformation; } /** @internal */ diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 5d94b6516c2ba..df4ba23244b4d 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -408,6 +408,8 @@ export interface IIndexPattern { // (undocumented) fields: IFieldType[]; // (undocumented) + getTimeField?(): IFieldType | undefined; + // (undocumented) id?: string; // (undocumented) timeFieldName?: string; diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts new file mode 100644 index 0000000000000..5ea151dffdc8e --- /dev/null +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts @@ -0,0 +1,93 @@ +/* + * 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 expect from '@kbn/expect'; +import { ExpectExpression, expectExpressionProvider } from './helpers'; +import { FtrProviderContext } from '../../../functional/ftr_provider_context'; + +function getCell(esaggsResult: any, column: number, row: number): unknown | undefined { + const columnId = esaggsResult?.columns[column]?.id; + if (!columnId) { + return; + } + return esaggsResult?.rows[row]?.[columnId]; +} + +export default function({ + getService, + updateBaselines, +}: FtrProviderContext & { updateBaselines: boolean }) { + let expectExpression: ExpectExpression; + describe('esaggs pipeline expression tests', () => { + before(() => { + expectExpression = expectExpressionProvider({ getService, updateBaselines }); + }); + + describe('correctly renders tagcloud', () => { + it('filters on index pattern primary date field by default', async () => { + const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }]; + const timeRange = { + from: '2006-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + const expression = ` + kibana_context timeRange='${JSON.stringify(timeRange)}' + | esaggs index='logstash-*' aggConfigs='${JSON.stringify(aggConfigs)}' + `; + const result = await expectExpression('esaggs_primary_timefield', expression).getResponse(); + expect(getCell(result, 0, 0)).to.be(9375); + }); + + it('filters on the specified date field', async () => { + const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }]; + const timeRange = { + from: '2006-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + const expression = ` + kibana_context timeRange='${JSON.stringify(timeRange)}' + | esaggs index='logstash-*' timeFields='relatedContent.article:published_time' aggConfigs='${JSON.stringify( + aggConfigs + )}' + `; + const result = await expectExpression('esaggs_other_timefield', expression).getResponse(); + expect(getCell(result, 0, 0)).to.be(11134); + }); + + it('filters on multiple specified date field', async () => { + const aggConfigs = [{ id: 1, enabled: true, type: 'count', schema: 'metric', params: {} }]; + const timeRange = { + from: '2006-09-21T00:00:00Z', + to: '2015-09-22T00:00:00Z', + }; + const expression = ` + kibana_context timeRange='${JSON.stringify(timeRange)}' + | esaggs index='logstash-*' timeFields='relatedContent.article:published_time' timeFields='@timestamp' aggConfigs='${JSON.stringify( + aggConfigs + )}' + `; + const result = await expectExpression( + 'esaggs_multiple_timefields', + expression + ).getResponse(); + expect(getCell(result, 0, 0)).to.be(7452); + }); + }); + }); +} diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.ts b/test/interpreter_functional/test_suites/run_pipeline/index.ts index 031a0e3576ccc..9590f9f8c1794 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/index.ts @@ -46,5 +46,6 @@ export default function({ getService, getPageObjects, loadTestFile }: FtrProvide loadTestFile(require.resolve('./basic')); loadTestFile(require.resolve('./tag_cloud')); loadTestFile(require.resolve('./metric')); + loadTestFile(require.resolve('./esaggs')); }); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts deleted file mode 100644 index 5f35ef650a08c..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.test.ts +++ /dev/null @@ -1,83 +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 { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; -import { getAutoDate } from './auto_date'; - -describe('auto_date', () => { - let autoDate: ReturnType; - - beforeEach(() => { - autoDate = getAutoDate({ data: dataPluginMock.createSetupContract() }); - }); - - it('should do nothing if no time range is provided', () => { - const result = autoDate.fn( - { - type: 'kibana_context', - }, - { - aggConfigs: 'canttouchthis', - }, - // eslint-disable-next-line - {} as any - ); - - expect(result).toEqual('canttouchthis'); - }); - - it('should not change anything if there are no auto date histograms', () => { - const aggConfigs = JSON.stringify([ - { type: 'date_histogram', params: { interval: '35h' } }, - { type: 'count' }, - ]); - const result = autoDate.fn( - { - timeRange: { - from: 'now-10d', - to: 'now', - }, - type: 'kibana_context', - }, - { - aggConfigs, - }, - // eslint-disable-next-line - {} as any - ); - - expect(result).toEqual(aggConfigs); - }); - - it('should change auto date histograms', () => { - const aggConfigs = JSON.stringify([ - { type: 'date_histogram', params: { interval: 'auto' } }, - { type: 'count' }, - ]); - const result = autoDate.fn( - { - timeRange: { - from: 'now-10d', - to: 'now', - }, - type: 'kibana_context', - }, - { - aggConfigs, - }, - // eslint-disable-next-line - {} as any - ); - - const interval = JSON.parse(result).find( - (agg: { type: string }) => agg.type === 'date_histogram' - ).params.interval; - - expect(interval).toBeTruthy(); - expect(typeof interval).toEqual('string'); - expect(interval).not.toEqual('auto'); - }); -}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts b/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts deleted file mode 100644 index 97a46f4a3e176..0000000000000 --- a/x-pack/plugins/lens/public/indexpattern_datasource/auto_date.ts +++ /dev/null @@ -1,79 +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 { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; -import { - ExpressionFunctionDefinition, - KibanaContext, -} from '../../../../../src/plugins/expressions/public'; - -interface LensAutoDateProps { - aggConfigs: string; -} - -export function getAutoDate(deps: { - data: DataPublicPluginSetup; -}): ExpressionFunctionDefinition< - 'lens_auto_date', - KibanaContext | null, - LensAutoDateProps, - string -> { - function autoIntervalFromContext(ctx?: KibanaContext | null) { - if (!ctx || !ctx.timeRange) { - return; - } - - return deps.data.search.aggs.calculateAutoTimeExpression(ctx.timeRange); - } - - /** - * Convert all 'auto' date histograms into a concrete value (e.g. 2h). - * This allows us to support 'auto' on all date fields, and opens the - * door to future customizations (e.g. adjusting the level of detail, etc). - */ - return { - name: 'lens_auto_date', - aliases: [], - help: '', - inputTypes: ['kibana_context', 'null'], - args: { - aggConfigs: { - types: ['string'], - default: '""', - help: '', - }, - }, - fn(input, args) { - const interval = autoIntervalFromContext(input); - - if (!interval) { - return args.aggConfigs; - } - - const configs = JSON.parse(args.aggConfigs) as Array<{ - type: string; - params: { interval: string }; - }>; - - const updatedConfigs = configs.map(c => { - if (c.type !== 'date_histogram' || !c.params || c.params.interval !== 'auto') { - return c; - } - - return { - ...c, - params: { - ...c.params, - interval, - }, - }; - }); - - return JSON.stringify(updatedConfigs); - }, - }; -} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index fe14f472341af..73fd144b9c7f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -8,7 +8,6 @@ import { CoreSetup } from 'kibana/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { getIndexPatternDatasource } from './indexpattern'; import { renameColumns } from './rename_columns'; -import { getAutoDate } from './auto_date'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { DataPublicPluginSetup, @@ -31,10 +30,9 @@ export class IndexPatternDatasource { setup( core: CoreSetup, - { data: dataSetup, expressions, editorFrame }: IndexPatternDatasourceSetupPlugins + { expressions, editorFrame }: IndexPatternDatasourceSetupPlugins ) { expressions.registerFunction(renameColumns); - expressions.registerFunction(getAutoDate({ data: dataSetup })); editorFrame.registerDatasource( core.getStartServices().then(([coreStart, { data }]) => diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index e4f3677d0fe88..06635e663361d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -10,6 +10,7 @@ import { DatasourcePublicAPI, Operation, Datasource } from '../types'; import { coreMock } from 'src/core/public/mocks'; import { IndexPatternPersistedState, IndexPatternPrivateState } from './types'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { Ast } from '@kbn/interpreter/common'; jest.mock('./loader'); jest.mock('../id_generator'); @@ -262,20 +263,7 @@ describe('IndexPattern Data Source', () => { Object { "arguments": Object { "aggConfigs": Array [ - Object { - "chain": Array [ - Object { - "arguments": Object { - "aggConfigs": Array [ - "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]", - ], - }, - "function": "lens_auto_date", - "type": "function", - }, - ], - "type": "expression", - }, + "[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]", ], "includeFormatHints": Array [ true, @@ -289,6 +277,9 @@ describe('IndexPattern Data Source', () => { "partialRows": Array [ false, ], + "timeFields": Array [ + "timestamp", + ], }, "function": "esaggs", "type": "function", @@ -307,6 +298,89 @@ describe('IndexPattern Data Source', () => { } `); }); + + it('should put all time fields used in date_histograms to the esaggs timeFields parameter', async () => { + const queryPersistedState: IndexPatternPersistedState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: { + label: 'Count of records', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + col2: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'auto', + }, + }, + col3: { + label: 'Date 2', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'another_datefield', + params: { + interval: 'auto', + }, + }, + }, + }, + }, + }; + + const state = stateFromPersistedState(queryPersistedState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']); + }); + + it('should not put date fields used outside date_histograms to the esaggs timeFields parameter', async () => { + const queryPersistedState: IndexPatternPersistedState = { + currentIndexPatternId: '1', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'Count of records', + dataType: 'date', + isBucketed: false, + sourceField: 'timefield', + operationType: 'cardinality', + }, + col2: { + label: 'Date', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'auto', + }, + }, + }, + }, + }, + }; + + const state = stateFromPersistedState(queryPersistedState); + + const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; + expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp']); + expect(ast.chain[0].arguments.timeFields).not.toContain('timefield'); + }); }); describe('#insertLayer', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 3ab51b5fa3f2b..1308fa3b7ca60 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -10,6 +10,7 @@ import { IndexPatternColumn } from './indexpattern'; import { operationDefinitionMap } from './operations'; import { IndexPattern, IndexPatternPrivateState } from './types'; import { OriginalColumn } from './rename_columns'; +import { dateHistogramOperation } from './operations/definitions'; function getExpressionForLayer( indexPattern: IndexPattern, @@ -68,6 +69,12 @@ function getExpressionForLayer( return base; }); + const allDateHistogramFields = Object.values(columns) + .map(column => + column.operationType === dateHistogramOperation.type ? column.sourceField : null + ) + .filter((field): field is string => Boolean(field)); + return { type: 'expression', chain: [ @@ -79,20 +86,8 @@ function getExpressionForLayer( metricsAtAllLevels: [false], partialRows: [false], includeFormatHints: [true], - aggConfigs: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_auto_date', - arguments: { - aggConfigs: [JSON.stringify(aggs)], - }, - }, - ], - }, - ], + timeFields: allDateHistogramFields, + aggConfigs: [JSON.stringify(aggs)], }, }, { diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts index e80308cc9acdb..4cc330d40efd7 100644 --- a/x-pack/plugins/lens/server/migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations.test.ts @@ -158,4 +158,124 @@ describe('Lens migrations', () => { ]); }); }); + + describe('7.8.0 auto timestamp', () => { + const context = {} as SavedObjectMigrationContext; + + const example = { + type: 'lens', + attributes: { + expression: `kibana + | kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]" + | lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c" + tables={esaggs + index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" + metricsAtAllLevels=false + partialRows=false + includeFormatHints=true + aggConfigs={ + lens_auto_date + aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" + } + | lens_rename_columns idMap="{\\"col-0-1d9cc16c-1460-41de-88f8-471932ecbc97\\":{\\"label\\":\\"products.created_on\\",\\"dataType\\":\\"date\\",\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"products.created_on\\",\\"isBucketed\\":true,\\"scale\\":\\"interval\\",\\"params\\":{\\"interval\\":\\"auto\\"},\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\"},\\"col-1-66115819-8481-4917-a6dc-8ffb10dd02df\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"operationType\\":\\"count\\",\\"suggestedPriority\\":0,\\"isBucketed\\":false,\\"scale\\":\\"ratio\\",\\"sourceField\\":\\"Records\\",\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\"}}" + } + | lens_xy_chart + xTitle="products.created_on" + yTitle="Count of records" + legend={lens_xy_legendConfig isVisible=true position="right"} + layers={lens_xy_layer + layerId="bd09dc71-a7e2-42d0-83bd-85df8291f03c" + hide=false + xAccessor="1d9cc16c-1460-41de-88f8-471932ecbc97" + yScaleType="linear" + xScaleType="time" + isHistogram=true + seriesType="bar_stacked" + accessors="66115819-8481-4917-a6dc-8ffb10dd02df" + columnToLabel="{\\"66115819-8481-4917-a6dc-8ffb10dd02df\\":\\"Count of records\\"}" + } + `, + state: { + datasourceStates: { + indexpattern: { + currentIndexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + layers: { + 'bd09dc71-a7e2-42d0-83bd-85df8291f03c': { + indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + columns: { + '1d9cc16c-1460-41de-88f8-471932ecbc97': { + label: 'products.created_on', + dataType: 'date', + operationType: 'date_histogram', + sourceField: 'products.created_on', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '66115819-8481-4917-a6dc-8ffb10dd02df': { + label: 'Count of records', + dataType: 'number', + operationType: 'count', + suggestedPriority: 0, + isBucketed: false, + scale: 'ratio', + sourceField: 'Records', + }, + }, + columnOrder: [ + '1d9cc16c-1460-41de-88f8-471932ecbc97', + '66115819-8481-4917-a6dc-8ffb10dd02df', + ], + }, + }, + }, + }, + datasourceMetaData: { + filterableIndexPatterns: [ + { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', title: 'kibana_sample_data_ecommerce' }, + ], + }, + visualization: { + legend: { isVisible: true, position: 'right' }, + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: 'bd09dc71-a7e2-42d0-83bd-85df8291f03c', + accessors: ['66115819-8481-4917-a6dc-8ffb10dd02df'], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + xAccessor: '1d9cc16c-1460-41de-88f8-471932ecbc97', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + title: 'Bar chart', + visualizationType: 'lnsXY', + }, + }; + + it('should remove the lens_auto_date expression', () => { + const result = migrations['7.8.0'](example, context); + expect(result.attributes.expression).toContain(`timeFields=\"products.created_on\"`); + }); + + it('should handle pre-migrated expression', () => { + const input = { + type: 'lens', + attributes: { + ...example.attributes, + expression: `kibana +| kibana_context query="{\\"query\\":\\"\\",\\"language\\":\\"kuery\\"}" filters="[]" +| lens_merge_tables layerIds="bd09dc71-a7e2-42d0-83bd-85df8291f03c" + tables={esaggs index="ff959d40-b880-11e8-a6d9-e546fe2bba5f" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs="[{\\"id\\":\\"1d9cc16c-1460-41de-88f8-471932ecbc97\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"products.created_on\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"auto\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}},{\\"id\\":\\"66115819-8481-4917-a6dc-8ffb10dd02df\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}}]" timeFields=\"products.created_on\"} +| lens_xy_chart xTitle="products.created_on" yTitle="Count of records" legend={lens_xy_legendConfig isVisible=true position="right"} layers={}`, + }, + }; + const result = migrations['7.8.0'](input, context); + expect(result).toEqual(input); + }); + }); }); diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index 3d238723b7438..51fcd3b6198c3 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, flow } from 'lodash'; +import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { SavedObjectMigrationFn } from 'src/core/server'; interface XYLayerPre77 { @@ -14,6 +15,122 @@ interface XYLayerPre77 { accessors: string[]; } +/** + * Removes the `lens_auto_date` subexpression from a stored expression + * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"} + */ +const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { + const expression: string = doc.attributes?.expression; + try { + const ast = fromExpression(expression); + const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => { + if (topNode.function !== 'lens_merge_tables') { + return topNode; + } + return { + ...topNode, + arguments: { + ...topNode.arguments, + tables: (topNode.arguments.tables as Ast[]).map(middleNode => { + return { + type: 'expression', + chain: middleNode.chain.map(node => { + // Check for sub-expression in aggConfigs + if ( + node.function === 'esaggs' && + typeof node.arguments.aggConfigs[0] !== 'string' + ) { + return { + ...node, + arguments: { + ...node.arguments, + aggConfigs: (node.arguments.aggConfigs[0] as Ast).chain[0].arguments + .aggConfigs, + }, + }; + } + return node; + }), + }; + }), + }, + }; + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + expression: toExpression({ ...ast, chain: newChain }), + }, + }; + } catch (e) { + context.log.warning(e.message); + return { ...doc }; + } +}; + +/** + * Adds missing timeField arguments to esaggs in the Lens expression + */ +const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { + const expression: string = doc.attributes?.expression; + + try { + const ast = fromExpression(expression); + const newChain: ExpressionFunctionAST[] = ast.chain.map(topNode => { + if (topNode.function !== 'lens_merge_tables') { + return topNode; + } + return { + ...topNode, + arguments: { + ...topNode.arguments, + tables: (topNode.arguments.tables as Ast[]).map(middleNode => { + return { + type: 'expression', + chain: middleNode.chain.map(node => { + // Skip if there are any timeField arguments already, because that indicates + // the fix is already applied + if (node.function !== 'esaggs' || node.arguments.timeFields) { + return node; + } + const timeFields: string[] = []; + JSON.parse(node.arguments.aggConfigs[0] as string).forEach( + (agg: { type: string; params: { field: string } }) => { + if (agg.type !== 'date_histogram') { + return; + } + timeFields.push(agg.params.field); + } + ); + return { + ...node, + arguments: { + ...node.arguments, + timeFields, + }, + }; + }), + }; + }), + }, + }; + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + expression: toExpression({ ...ast, chain: newChain }), + }, + }; + } catch (e) { + context.log.warning(e.message); + return { ...doc }; + } +}; + export const migrations: Record = { '7.7.0': doc => { const newDoc = cloneDeep(doc); @@ -34,4 +151,7 @@ export const migrations: Record = { } return newDoc; }, + // The order of these migrations matter, since the timefield migration relies on the aggConfigs + // sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was). + '7.8.0': flow(removeLensAutoDate, addTimeFieldToEsaggs), }; From fba5128bd85d66c346abd1d3762ac6c57cd4a777 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 29 Apr 2020 18:03:30 -0700 Subject: [PATCH 018/122] [Ingest] Edit datasource UI (#64727) * Adjust NewDatasource type to exclude stream `agent_stream` property, add additional datasource hooks * Initial pass at edit datasource UI * Clean up dupe code, fix submit button not enabled after re-selecting a package * Remove delete config functionality from list page * Show validation errors for data source name and description fields * Fix types * Add success toasts * Review fixes, clean up i18n Co-authored-by: Elastic Machine --- .../datasource_to_agent_datasource.test.ts | 21 +- .../datasource_to_agent_datasource.ts | 8 +- .../common/types/models/agent_config.ts | 4 +- .../common/types/models/datasource.ts | 18 +- .../hooks/use_request/datasource.ts | 26 +- .../components/layout.tsx | 24 +- .../create_datasource_page/index.tsx | 26 +- .../step_configure_datasource.tsx | 62 +--- .../step_define_datasource.tsx | 13 +- .../create_datasource_page/types.ts | 3 +- .../datasources/datasources_table.tsx | 44 +-- .../edit_datasource_page/index.tsx | 323 ++++++++++++++++++ .../sections/agent_config/index.tsx | 4 + .../sections/agent_config/list_page/index.tsx | 69 +--- .../ingest_manager/types/index.ts | 2 + .../server/routes/agent_config/handlers.ts | 3 +- .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - 18 files changed, 462 insertions(+), 198 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts index a7d4e36d16f2a..bff799798ff6e 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts @@ -3,11 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Datasource, NewDatasource, DatasourceInput } from '../types'; +import { Datasource, DatasourceInput } from '../types'; import { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource'; describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { - const mockNewDatasource: NewDatasource = { + const mockDatasource: Datasource = { + id: 'some-uuid', name: 'mock-datasource', description: '', config_id: '', @@ -15,11 +16,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { output_id: '', namespace: 'default', inputs: [], - }; - - const mockDatasource: Datasource = { - ...mockNewDatasource, - id: 'some-uuid', revision: 1, }; @@ -107,17 +103,6 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }); }); - it('uses name for id when id is not provided in case of new datasource', () => { - expect(storedDatasourceToAgentDatasource(mockNewDatasource)).toEqual({ - id: 'mock-datasource', - name: 'mock-datasource', - namespace: 'default', - enabled: true, - use_output: 'default', - inputs: [], - }); - }); - it('returns agent datasource config with flattened input and package stream', () => { expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({ id: 'some-uuid', diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts index 5deb33ccf10f1..620b663451ea3 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts @@ -3,16 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Datasource, NewDatasource, FullAgentConfigDatasource } from '../types'; +import { Datasource, FullAgentConfigDatasource } from '../types'; import { DEFAULT_OUTPUT } from '../constants'; export const storedDatasourceToAgentDatasource = ( - datasource: Datasource | NewDatasource + datasource: Datasource ): FullAgentConfigDatasource => { - const { name, namespace, enabled, package: pkg, inputs } = datasource; + const { id, name, namespace, enabled, package: pkg, inputs } = datasource; const fullDatasource: FullAgentConfigDatasource = { - id: 'id' in datasource ? datasource.id : name, + id: id || name, name, namespace, enabled, diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts index 7705956590c16..96121251b133e 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.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 { SavedObjectAttributes } from 'src/core/public'; import { Datasource, DatasourcePackage, @@ -26,7 +24,7 @@ export interface NewAgentConfig { monitoring_enabled?: Array<'logs' | 'metrics'>; } -export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes { +export interface AgentConfig extends NewAgentConfig { id: string; status: AgentConfigStatus; datasources: string[] | Datasource[]; diff --git a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts index 885e0a9316d79..ca61a93d9be93 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts @@ -17,22 +17,29 @@ export interface DatasourceConfigRecordEntry { export type DatasourceConfigRecord = Record; -export interface DatasourceInputStream { +export interface NewDatasourceInputStream { id: string; enabled: boolean; dataset: string; processors?: string[]; config?: DatasourceConfigRecord; vars?: DatasourceConfigRecord; +} + +export interface DatasourceInputStream extends NewDatasourceInputStream { agent_stream?: any; } -export interface DatasourceInput { +export interface NewDatasourceInput { type: string; enabled: boolean; processors?: string[]; config?: DatasourceConfigRecord; vars?: DatasourceConfigRecord; + streams: NewDatasourceInputStream[]; +} + +export interface DatasourceInput extends Omit { streams: DatasourceInputStream[]; } @@ -44,10 +51,11 @@ export interface NewDatasource { enabled: boolean; package?: DatasourcePackage; output_id: string; - inputs: DatasourceInput[]; + inputs: NewDatasourceInput[]; } -export type Datasource = NewDatasource & { +export interface Datasource extends Omit { id: string; + inputs: DatasourceInput[]; revision: number; -}; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts index 0d19ecd0cb735..e2fc190e158f9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts @@ -5,12 +5,18 @@ */ import { sendRequest, useRequest } from './use_request'; import { datasourceRouteService } from '../../services'; -import { CreateDatasourceRequest, CreateDatasourceResponse } from '../../types'; +import { + CreateDatasourceRequest, + CreateDatasourceResponse, + UpdateDatasourceRequest, + UpdateDatasourceResponse, +} from '../../types'; import { DeleteDatasourcesRequest, DeleteDatasourcesResponse, GetDatasourcesRequest, GetDatasourcesResponse, + GetOneDatasourceResponse, } from '../../../../../common/types/rest_spec'; export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => { @@ -21,6 +27,17 @@ export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => { }); }; +export const sendUpdateDatasource = ( + datasourceId: string, + body: UpdateDatasourceRequest['body'] +) => { + return sendRequest({ + path: datasourceRouteService.getUpdatePath(datasourceId), + method: 'put', + body: JSON.stringify(body), + }); +}; + export const sendDeleteDatasource = (body: DeleteDatasourcesRequest['body']) => { return sendRequest({ path: datasourceRouteService.getDeletePath(), @@ -36,3 +53,10 @@ export function useGetDatasources(query: GetDatasourcesRequest['query']) { query, }); } + +export const sendGetOneDatasource = (datasourceId: string) => { + return sendRequest({ + path: datasourceRouteService.getInfoPath(datasourceId), + method: 'get', + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx index 39d882f7fdf65..f1e3fea6a0742 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx @@ -39,17 +39,29 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{

- + {from === 'edit' ? ( + + ) : ( + + )}

- {from === 'config' ? ( + {from === 'edit' ? ( + + ) : from === 'config' ? ( - {agentConfig && from === 'config' ? ( + {agentConfig && (from === 'config' || from === 'edit') ? ( { const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => { if (updatedPackageInfo) { setPackageInfo(updatedPackageInfo); + setFormState('VALID'); } else { setFormState('INVALID'); setPackageInfo(undefined); @@ -152,9 +153,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const cancelUrl = from === 'config' ? CONFIG_URL : PACKAGE_URL; // Save datasource - const [formState, setFormState] = useState< - 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED' - >('INVALID'); + const [formState, setFormState] = useState('INVALID'); const saveDatasource = async () => { setFormState('LOADING'); const result = await sendCreateDatasource(datasource); @@ -174,6 +173,23 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const { error } = await saveDatasource(); if (!error) { history.push(`${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}`); + notifications.toasts.addSuccess({ + title: i18n.translate('xpack.ingestManager.createDatasource.addedNotificationTitle', { + defaultMessage: `Successfully added '{datasourceName}'`, + values: { + datasourceName: datasource.name, + }, + }), + text: + agentCount && agentConfig + ? i18n.translate('xpack.ingestManager.createDatasource.addedNotificationMessage', { + defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`, + values: { + agentConfigName: agentConfig.name, + }, + }) + : undefined, + }); } else { notifications.toasts.addError(error, { title: 'Error', @@ -229,6 +245,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { packageInfo={packageInfo} datasource={datasource} updateDatasource={updateDatasource} + validationResults={validationResults!} /> ) : null, }, @@ -240,7 +257,6 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { children: agentConfig && packageInfo ? ( ) => void; validationResults: DatasourceValidationResults; submitAttempted: boolean; -}> = ({ - agentConfig, - packageInfo, - datasource, - updateDatasource, - validationResults, - submitAttempted, -}) => { - // Form show/hide states - +}> = ({ packageInfo, datasource, updateDatasource, validationResults, submitAttempted }) => { const hasErrors = validationResults ? validationHasErrors(validationResults) : false; - // Update datasource's package and config info - useEffect(() => { - const dsPackage = datasource.package; - const currentPkgKey = dsPackage ? `${dsPackage.name}-${dsPackage.version}` : ''; - const pkgKey = `${packageInfo.name}-${packageInfo.version}`; - - // If package has changed, create shell datasource with input&stream values based on package info - if (currentPkgKey !== pkgKey) { - // Existing datasources on the agent config using the package name, retrieve highest number appended to datasource name - const dsPackageNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`); - const dsWithMatchingNames = (agentConfig.datasources as Datasource[]) - .filter(ds => Boolean(ds.name.match(dsPackageNamePattern))) - .map(ds => parseInt(ds.name.match(dsPackageNamePattern)![1], 10)) - .sort(); - - updateDatasource({ - name: `${packageInfo.name}-${ - dsWithMatchingNames.length ? dsWithMatchingNames[dsWithMatchingNames.length - 1] + 1 : 1 - }`, - package: { - name: packageInfo.name, - title: packageInfo.title, - version: packageInfo.version, - }, - inputs: packageToConfigDatasourceInputs(packageInfo), - }); - } - - // If agent config has changed, update datasource's config ID and namespace - if (datasource.config_id !== agentConfig.id) { - updateDatasource({ - config_id: agentConfig.id, - namespace: agentConfig.namespace, - }); - } - }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); - - // Step B, configure inputs (and their streams) + // Configure inputs (and their streams) // Assume packages only export one datasource for now const renderConfigureInputs = () => packageInfo.datasources && diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx index 792389381eaf0..c4d602c2c2081 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx @@ -17,13 +17,16 @@ import { } from '@elastic/eui'; import { AgentConfig, PackageInfo, Datasource, NewDatasource } from '../../../types'; import { packageToConfigDatasourceInputs } from '../../../services'; +import { Loading } from '../../../components'; +import { DatasourceValidationResults } from './services'; export const StepDefineDatasource: React.FunctionComponent<{ agentConfig: AgentConfig; packageInfo: PackageInfo; datasource: NewDatasource; updateDatasource: (fields: Partial) => void; -}> = ({ agentConfig, packageInfo, datasource, updateDatasource }) => { + validationResults: DatasourceValidationResults; +}> = ({ agentConfig, packageInfo, datasource, updateDatasource, validationResults }) => { // Form show/hide states const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState(false); @@ -64,11 +67,13 @@ export const StepDefineDatasource: React.FunctionComponent<{ } }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); - return ( + return validationResults ? ( <> } + isInvalid={!!validationResults.description} + error={validationResults.description} > ) : null} + ) : ( + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts index 85cc758fc4c46..10b30a5696d83 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export type CreateDatasourceFrom = 'package' | 'config'; +export type CreateDatasourceFrom = 'package' | 'config' | 'edit'; +export type DatasourceFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx index 1eee9f6b0c346..a0418c5f256c4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx @@ -19,7 +19,7 @@ import { import { AgentConfig, Datasource } from '../../../../../types'; import { TableRowActions } from '../../../components/table_row_actions'; import { DangerEuiContextMenuItem } from '../../../components/danger_eui_context_menu_item'; -import { useCapabilities } from '../../../../../hooks'; +import { useCapabilities, useLink } from '../../../../../hooks'; import { useAgentConfigLink } from '../../hooks/use_details_uri'; import { DatasourceDeleteProvider } from '../../../components/datasource_delete_provider'; import { useConfigRefresh } from '../../hooks/use_config'; @@ -56,6 +56,7 @@ export const DatasourcesTable: React.FunctionComponent = ({ }) => { const hasWriteCapabilities = useCapabilities().write; const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id }); + const editDatasourceLink = useLink(`/configs/${config.id}/edit-datasource`); const refreshConfig = useConfigRefresh(); // With the datasources provided on input, generate the list of datasources @@ -201,22 +202,21 @@ export const DatasourcesTable: React.FunctionComponent = ({ {}} + // key="datasourceView" + // > + // + // , {}} - key="datasourceView" - > - - , - // FIXME: implement Edit datasource action - {}} + href={`${editDatasourceLink}/${datasource.id}`} key="datasourceEdit" > = ({ /> , // FIXME: implement Copy datasource action - {}} key="datasourceCopy"> - - , + // {}} key="datasourceCopy"> + // + // , {deleteDatasourcePrompt => { return ( @@ -256,7 +256,7 @@ export const DatasourcesTable: React.FunctionComponent = ({ ], }, ], - [config, hasWriteCapabilities, refreshConfig] + [config, editDatasourceLink, hasWriteCapabilities, refreshConfig] ); return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx new file mode 100644 index 0000000000000..d4c39f21a1ea6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx @@ -0,0 +1,323 @@ +/* + * 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, { useState, useEffect } from 'react'; +import { useRouteMatch, useHistory } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButtonEmpty, + EuiButton, + EuiSteps, + EuiBottomBar, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; +import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; +import { + useLink, + useCore, + useConfig, + sendUpdateDatasource, + sendGetAgentStatus, + sendGetOneAgentConfig, + sendGetOneDatasource, + sendGetPackageInfoByKey, +} from '../../../hooks'; +import { Loading, Error } from '../../../components'; +import { + CreateDatasourcePageLayout, + ConfirmCreateDatasourceModal, +} from '../create_datasource_page/components'; +import { + DatasourceValidationResults, + validateDatasource, + validationHasErrors, +} from '../create_datasource_page/services'; +import { DatasourceFormState, CreateDatasourceFrom } from '../create_datasource_page/types'; +import { StepConfigureDatasource } from '../create_datasource_page/step_configure_datasource'; +import { StepDefineDatasource } from '../create_datasource_page/step_define_datasource'; + +export const EditDatasourcePage: React.FunctionComponent = () => { + const { notifications } = useCore(); + const { + fleet: { enabled: isFleetEnabled }, + } = useConfig(); + const { + params: { configId, datasourceId }, + } = useRouteMatch(); + const history = useHistory(); + + // Agent config, package info, and datasource states + const [isLoadingData, setIsLoadingData] = useState(true); + const [loadingError, setLoadingError] = useState(); + const [agentConfig, setAgentConfig] = useState(); + const [packageInfo, setPackageInfo] = useState(); + const [datasource, setDatasource] = useState({ + name: '', + description: '', + config_id: '', + enabled: true, + output_id: '', + inputs: [], + }); + + // Retrieve agent config, package, and datasource info + useEffect(() => { + const getData = async () => { + setIsLoadingData(true); + setLoadingError(undefined); + try { + const [{ data: agentConfigData }, { data: datasourceData }] = await Promise.all([ + sendGetOneAgentConfig(configId), + sendGetOneDatasource(datasourceId), + ]); + if (agentConfigData?.item) { + setAgentConfig(agentConfigData.item); + } + if (datasourceData?.item) { + const { id, revision, inputs, ...restOfDatasource } = datasourceData.item; + // Remove `agent_stream` from all stream info, we assign this after saving + const newDatasource = { + ...restOfDatasource, + inputs: inputs.map(input => { + const { streams, ...restOfInput } = input; + return { + ...restOfInput, + streams: streams.map(stream => { + const { agent_stream, ...restOfStream } = stream; + return restOfStream; + }), + }; + }), + }; + setDatasource(newDatasource); + if (datasourceData.item.package) { + const { data: packageData } = await sendGetPackageInfoByKey( + `${datasourceData.item.package.name}-${datasourceData.item.package.version}` + ); + if (packageData?.response) { + setPackageInfo(packageData.response); + setValidationResults(validateDatasource(newDatasource, packageData.response)); + setFormState('VALID'); + } + } + } + } catch (e) { + setLoadingError(e); + } + setIsLoadingData(false); + }; + getData(); + }, [configId, datasourceId]); + + // Retrieve agent count + const [agentCount, setAgentCount] = useState(0); + useEffect(() => { + const getAgentCount = async () => { + const { data } = await sendGetAgentStatus({ configId }); + if (data?.results.total) { + setAgentCount(data.results.total); + } + }; + + if (isFleetEnabled) { + getAgentCount(); + } + }, [configId, isFleetEnabled]); + + // Datasource validation state + const [validationResults, setValidationResults] = useState(); + const hasErrors = validationResults ? validationHasErrors(validationResults) : false; + + // Update datasource method + const updateDatasource = (updatedFields: Partial) => { + const newDatasource = { + ...datasource, + ...updatedFields, + }; + setDatasource(newDatasource); + + // eslint-disable-next-line no-console + console.debug('Datasource updated', newDatasource); + const newValidationResults = updateDatasourceValidation(newDatasource); + const hasValidationErrors = newValidationResults + ? validationHasErrors(newValidationResults) + : false; + if (!hasValidationErrors) { + setFormState('VALID'); + } + }; + + const updateDatasourceValidation = (newDatasource?: NewDatasource) => { + if (packageInfo) { + const newValidationResult = validateDatasource(newDatasource || datasource, packageInfo); + setValidationResults(newValidationResult); + // eslint-disable-next-line no-console + console.debug('Datasource validation results', newValidationResult); + + return newValidationResult; + } + }; + + // Cancel url + const CONFIG_URL = useLink(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); + const cancelUrl = CONFIG_URL; + + // Save datasource + const [formState, setFormState] = useState('INVALID'); + const saveDatasource = async () => { + setFormState('LOADING'); + const result = await sendUpdateDatasource(datasourceId, datasource); + setFormState('SUBMITTED'); + return result; + }; + + const onSubmit = async () => { + if (formState === 'VALID' && hasErrors) { + setFormState('INVALID'); + return; + } + if (agentCount !== 0 && formState !== 'CONFIRM') { + setFormState('CONFIRM'); + return; + } + const { error } = await saveDatasource(); + if (!error) { + history.push(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); + notifications.toasts.addSuccess({ + title: i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationTitle', { + defaultMessage: `Successfully updated '{datasourceName}'`, + values: { + datasourceName: datasource.name, + }, + }), + text: + agentCount && agentConfig + ? i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationMessage', { + defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`, + values: { + agentConfigName: agentConfig.name, + }, + }) + : undefined, + }); + } else { + notifications.toasts.addError(error, { + title: 'Error', + }); + setFormState('VALID'); + } + }; + + const layoutProps = { + from: 'edit' as CreateDatasourceFrom, + cancelUrl, + agentConfig, + packageInfo, + }; + + return ( + + {isLoadingData ? ( + + ) : loadingError || !agentConfig || !packageInfo ? ( + + } + error={ + loadingError || + i18n.translate('xpack.ingestManager.editDatasource.errorLoadingDataMessage', { + defaultMessage: 'There was an error loading this data source information', + }) + } + /> + ) : ( + <> + {formState === 'CONFIRM' && ( + setFormState('VALID')} + /> + )} + + ), + }, + { + title: i18n.translate( + 'xpack.ingestManager.editDatasource.stepConfgiureDatasourceTitle', + { + defaultMessage: 'Select the data you want to collect', + } + ), + children: ( + + ), + }, + ]} + /> + + + + + + + + + + + + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx index 71ada155373bf..ef88aa5d17f1e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx @@ -8,10 +8,14 @@ import { HashRouter as Router, Switch, Route } from 'react-router-dom'; import { AgentConfigListPage } from './list_page'; import { AgentConfigDetailsPage } from './details_page'; import { CreateDatasourcePage } from './create_datasource_page'; +import { EditDatasourcePage } from './edit_datasource_page'; export const AgentConfigApp: React.FunctionComponent = () => ( + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 1ea162252c741..3dcc19bc4a5ae 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -36,13 +36,11 @@ import { useConfig, useUrlParams, } from '../../../hooks'; -import { AgentConfigDeleteProvider } from '../components'; import { CreateAgentConfigFlyout } from './components'; import { SearchBar } from '../../../components/search_bar'; import { LinkedAgentCount } from '../components'; import { useAgentConfigLink } from '../details_page/hooks/use_details_uri'; import { TableRowActions } from '../components/table_row_actions'; -import { DangerEuiContextMenuItem } from '../components/danger_eui_context_menu_item'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ overflow: 'hidden', @@ -108,30 +106,12 @@ const ConfigRowActions = memo<{ config: AgentConfig; onDelete: () => void }>( defaultMessage="Create data source" /> , - - - - , - - - {deleteAgentConfigsPrompt => { - return ( - deleteAgentConfigsPrompt([config.id], onDelete)} - > - - - ); - }} - , + // + // + // , ]} /> ); @@ -156,7 +136,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { : urlParams.kuery ?? '' ); const { pagination, pageSizeOptions, setPagination } = usePagination(); - const [selectedAgentConfigs, setSelectedAgentConfigs] = useState([]); const history = useHistory(); const isCreateAgentConfigFlyoutOpen = 'create' in urlParams; const setIsCreateAgentConfigFlyoutOpen = useCallback( @@ -321,34 +300,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { /> ) : null} - {selectedAgentConfigs.length ? ( - - - {deleteAgentConfigsPrompt => ( - { - deleteAgentConfigsPrompt( - selectedAgentConfigs.map(agentConfig => agentConfig.id), - () => { - sendRequest(); - setSelectedAgentConfigs([]); - } - ); - }} - > - - - )} - - - ) : null} = () => { items={agentConfigData ? agentConfigData.items : []} itemId="id" columns={columns} - isSelectable={true} - selection={{ - selectable: (agentConfig: AgentConfig) => !agentConfig.is_default, - onSelectionChange: (newSelectedAgentConfigs: AgentConfig[]) => { - setSelectedAgentConfigs(newSelectedAgentConfigs); - }, - }} + isSelectable={false} pagination={{ pageIndex: pagination.currentPage - 1, pageSize: pagination.pageSize, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 2f78ecd1b085e..1508f4dfaa628 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -32,6 +32,8 @@ export { // API schemas - Datasource CreateDatasourceRequest, CreateDatasourceResponse, + UpdateDatasourceRequest, + UpdateDatasourceResponse, // API schemas - Data Streams GetDataStreamsResponse, // API schemas - Agents diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts index 42298960cc615..69f14854cdd0f 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts @@ -22,6 +22,7 @@ import { } from '../../types'; import { GetAgentConfigsResponse, + GetAgentConfigsResponseItem, GetOneAgentConfigResponse, CreateAgentConfigResponse, UpdateAgentConfigResponse, @@ -46,7 +47,7 @@ export const getAgentConfigsHandler: RequestHandler< await bluebird.map( items, - agentConfig => + (agentConfig: GetAgentConfigsResponseItem) => listAgents(soClient, { showInactive: true, perPage: 0, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8974f0b5b4d58..81dc44f3a4cb4 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8225,11 +8225,8 @@ "xpack.ingestManager.agentConfigList.addButton": "エージェント構成を作成", "xpack.ingestManager.agentConfigList.agentsColumnTitle": "エージェント", "xpack.ingestManager.agentConfigList.clearFiltersLinkText": "フィルターを消去", - "xpack.ingestManager.agentConfigList.copyConfigActionText": "構成をコピー", "xpack.ingestManager.agentConfigList.createDatasourceActionText": "データソースを作成", "xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "データソース", - "xpack.ingestManager.agentConfigList.deleteButton": "{count, plural, one {# エージェント設定} other {# エージェント設定}}を削除", - "xpack.ingestManager.agentConfigList.deleteConfigActionText": "構成の削除", "xpack.ingestManager.agentConfigList.descriptionColumnTitle": "説明", "xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "エージェント構成を読み込み中...", "xpack.ingestManager.agentConfigList.nameColumnTitle": "名前", @@ -8313,7 +8310,6 @@ "xpack.ingestManager.configDetails.configDetailsTitle": "構成「{id}」", "xpack.ingestManager.configDetails.configNotFoundErrorTitle": "構成「{id}」が見つかりません", "xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "アクション", - "xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "データソースをコピー", "xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "データソースを削除", "xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "説明", "xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "データソースを編集", @@ -8321,7 +8317,6 @@ "xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "名前空間", "xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "パッケージ", "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "ストリーム", - "xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "データソースを表示", "xpack.ingestManager.configDetails.subTabs.datasouces": "データソース", "xpack.ingestManager.configDetails.subTabs.settings": "設定", "xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML ファイル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d36a62f15aee9..e06edb45de8fa 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8228,11 +8228,8 @@ "xpack.ingestManager.agentConfigList.addButton": "创建代理配置", "xpack.ingestManager.agentConfigList.agentsColumnTitle": "代理", "xpack.ingestManager.agentConfigList.clearFiltersLinkText": "清除筛选", - "xpack.ingestManager.agentConfigList.copyConfigActionText": "复制配置", "xpack.ingestManager.agentConfigList.createDatasourceActionText": "创建数据源", "xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "数据源", - "xpack.ingestManager.agentConfigList.deleteButton": "删除 {count, plural, one {# 个代理配置} other {# 个代理配置}}", - "xpack.ingestManager.agentConfigList.deleteConfigActionText": "删除配置", "xpack.ingestManager.agentConfigList.descriptionColumnTitle": "描述", "xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "正在加载代理配置……", "xpack.ingestManager.agentConfigList.nameColumnTitle": "名称", @@ -8316,7 +8313,6 @@ "xpack.ingestManager.configDetails.configDetailsTitle": "配置“{id}”", "xpack.ingestManager.configDetails.configNotFoundErrorTitle": "未找到配置“{id}”", "xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "操作", - "xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle": "复制数据源", "xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "删除数据源", "xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "描述", "xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "编辑数据源", @@ -8324,7 +8320,6 @@ "xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "命名空间", "xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "软件包", "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "流计数", - "xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle": "查看数据源", "xpack.ingestManager.configDetails.subTabs.datasouces": "数据源", "xpack.ingestManager.configDetails.subTabs.settings": "设置", "xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML 文件", From bcda1096e170b82c5bff02360b68b3255a46efb6 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Wed, 29 Apr 2020 19:58:27 -0600 Subject: [PATCH 019/122] [SIEM][Lists] Removes plugin dependencies, adds more unit tests, fixes more TypeScript types * Removes plugin dependencies for better integration outside of Requests such as alerting * Adds more unit tests * Fixes more TypeScript types to be more normalized * Makes this work with the user 'elastic' if security is turned off - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../plugins/lists/server/get_space_id.test.ts | 46 ++++ .../utils/get_space.ts => get_space_id.ts} | 4 +- x-pack/plugins/lists/server/get_user.test.ts | 67 ++++++ .../server/{services/utils => }/get_user.ts | 16 +- x-pack/plugins/lists/server/plugin.ts | 24 +-- .../services/items/buffer_lines.test.ts | 2 +- .../services/items/create_list_item.test.ts | 4 +- .../server/services/items/create_list_item.ts | 8 +- .../items/create_list_items_bulk.test.ts | 6 +- .../services/items/create_list_items_bulk.ts | 8 +- .../services/items/delete_list_item.test.ts | 11 +- .../server/services/items/delete_list_item.ts | 11 +- .../items/delete_list_item_by_value.test.ts | 2 +- .../items/delete_list_item_by_value.ts | 11 +- .../services/items/get_list_item.test.ts | 17 +- .../server/services/items/get_list_item.ts | 27 ++- .../services/items/get_list_item_by_value.ts | 9 +- .../items/get_list_item_by_values.test.ts | 21 +- .../services/items/get_list_item_by_values.ts | 29 ++- .../items/get_list_item_index.test.ts | 15 +- .../services/items/get_list_item_index.ts | 15 +- .../server/services/items/update_list_item.ts | 10 +- .../items/write_lines_to_bulk_list_items.ts | 17 +- .../items/write_list_items_to_stream.test.ts | 27 ++- .../items/write_list_items_to_stream.ts | 22 +- .../lists/server/services/lists/client.ts | 202 +++++++----------- .../server/services/lists/client_types.ts | 11 +- .../server/services/lists/create_list.test.ts | 4 +- .../server/services/lists/create_list.ts | 8 +- .../server/services/lists/delete_list.test.ts | 6 +- .../server/services/lists/delete_list.ts | 13 +- .../server/services/lists/get_list.test.ts | 10 +- .../lists/server/services/lists/get_list.ts | 8 +- .../services/lists/get_list_index.test.ts | 15 +- .../server/services/lists/get_list_index.ts | 12 +- .../server/services/lists/update_list.ts | 10 +- ...lient_mock.ts => get_call_cluster_mock.ts} | 17 +- .../get_create_list_item_bulk_options_mock.ts | 4 +- .../get_create_list_item_options_mock.ts | 4 +- .../mocks/get_create_list_options_mock.ts | 4 +- ..._delete_list_item_by_value_options_mock.ts | 4 +- .../get_delete_list_item_options_mock.ts | 4 +- .../mocks/get_delete_list_options_mock.ts | 4 +- ...mport_list_items_to_stream_options_mock.ts | 4 +- .../get_list_item_by_value_options_mock.ts | 4 +- .../get_list_item_by_values_options_mock.ts | 4 +- .../get_update_list_item_options_mock.ts | 4 +- .../mocks/get_update_list_options_mock.ts | 4 +- .../get_write_buffer_to_items_options_mock.ts | 4 +- ...write_list_items_to_stream_options_mock.ts | 8 +- .../lists/server/services/mocks/index.ts | 4 +- .../utils/derive_type_from_es_type.test.ts | 8 + .../get_query_filter_from_type_value.test.ts | 92 ++++++++ .../lists/server/services/utils/index.ts | 2 - .../transform_elastic_to_list_item.test.ts | 69 ++++++ .../utils/transform_elastic_to_list_item.ts | 13 +- ...ansform_list_item_to_elastic_query.test.ts | 47 ++++ .../transform_list_item_to_elastic_query.ts | 7 +- x-pack/plugins/lists/server/types.ts | 5 +- 59 files changed, 650 insertions(+), 398 deletions(-) create mode 100644 x-pack/plugins/lists/server/get_space_id.test.ts rename x-pack/plugins/lists/server/{services/utils/get_space.ts => get_space_id.ts} (83%) create mode 100644 x-pack/plugins/lists/server/get_user.test.ts rename x-pack/plugins/lists/server/{services/utils => }/get_user.ts (54%) rename x-pack/plugins/lists/server/services/mocks/{get_data_client_mock.ts => get_call_cluster_mock.ts} (57%) create mode 100644 x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts create mode 100644 x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts create mode 100644 x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts diff --git a/x-pack/plugins/lists/server/get_space_id.test.ts b/x-pack/plugins/lists/server/get_space_id.test.ts new file mode 100644 index 0000000000000..9c1d11b71984d --- /dev/null +++ b/x-pack/plugins/lists/server/get_space_id.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServerMock } from 'src/core/server/mocks'; +import { KibanaRequest } from 'src/core/server'; + +import { spacesServiceMock } from '../../spaces/server/spaces_service/spaces_service.mock'; + +import { getSpaceId } from './get_space_id'; + +describe('get_space_id', () => { + let request = KibanaRequest.from(httpServerMock.createRawRequest({})); + beforeEach(() => { + request = KibanaRequest.from(httpServerMock.createRawRequest({})); + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns "default" as the space id given a space id of "default"', () => { + const spaces = spacesServiceMock.createSetupContract(); + const space = getSpaceId({ request, spaces }); + expect(space).toEqual('default'); + }); + + test('it returns "another-space" as the space id given a space id of "another-space"', () => { + const spaces = spacesServiceMock.createSetupContract('another-space'); + const space = getSpaceId({ request, spaces }); + expect(space).toEqual('another-space'); + }); + + test('it returns "default" as the space id given a space id of undefined', () => { + const space = getSpaceId({ request, spaces: undefined }); + expect(space).toEqual('default'); + }); + + test('it returns "default" as the space id given a space id of null', () => { + const space = getSpaceId({ request, spaces: null }); + expect(space).toEqual('default'); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/get_space.ts b/x-pack/plugins/lists/server/get_space_id.ts similarity index 83% rename from x-pack/plugins/lists/server/services/utils/get_space.ts rename to x-pack/plugins/lists/server/get_space_id.ts index e23f963b2c40d..f224e37e04467 100644 --- a/x-pack/plugins/lists/server/services/utils/get_space.ts +++ b/x-pack/plugins/lists/server/get_space_id.ts @@ -6,9 +6,9 @@ import { KibanaRequest } from 'kibana/server'; -import { SpacesServiceSetup } from '../../../../spaces/server'; +import { SpacesServiceSetup } from '../../spaces/server'; -export const getSpace = ({ +export const getSpaceId = ({ spaces, request, }: { diff --git a/x-pack/plugins/lists/server/get_user.test.ts b/x-pack/plugins/lists/server/get_user.test.ts new file mode 100644 index 0000000000000..0992e3c361fcf --- /dev/null +++ b/x-pack/plugins/lists/server/get_user.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServerMock } from 'src/core/server/mocks'; +import { KibanaRequest } from 'src/core/server'; + +import { securityMock } from '../../security/server/mocks'; +import { SecurityPluginSetup } from '../../security/server'; + +import { getUser } from './get_user'; + +describe('get_user', () => { + let request = KibanaRequest.from(httpServerMock.createRawRequest({})); + beforeEach(() => { + jest.clearAllMocks(); + request = KibanaRequest.from(httpServerMock.createRawRequest({})); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns "bob" as the user given a security request with "bob"', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'bob' }); + const user = getUser({ request, security }); + expect(user).toEqual('bob'); + }); + + test('it returns "alice" as the user given a security request with "alice"', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'alice' }); + const user = getUser({ request, security }); + expect(user).toEqual('alice'); + }); + + test('it returns "elastic" as the user given null as the current user', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue(null); + const user = getUser({ request, security }); + expect(user).toEqual('elastic'); + }); + + test('it returns "elastic" as the user given undefined as the current user', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); + const user = getUser({ request, security }); + expect(user).toEqual('elastic'); + }); + + test('it returns "elastic" as the user given undefined as the plugin', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); + const user = getUser({ request, security: undefined }); + expect(user).toEqual('elastic'); + }); + + test('it returns "elastic" as the user given null as the plugin', () => { + const security: SecurityPluginSetup = securityMock.createSetup(); + security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined); + const user = getUser({ request, security: null }); + expect(user).toEqual('elastic'); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/get_user.ts b/x-pack/plugins/lists/server/get_user.ts similarity index 54% rename from x-pack/plugins/lists/server/services/utils/get_user.ts rename to x-pack/plugins/lists/server/get_user.ts index 1ddad047da722..3b59853d0ab62 100644 --- a/x-pack/plugins/lists/server/services/utils/get_user.ts +++ b/x-pack/plugins/lists/server/get_user.ts @@ -6,17 +6,21 @@ import { KibanaRequest } from 'kibana/server'; -import { SecurityPluginSetup } from '../../../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; -interface GetUserOptions { - security: SecurityPluginSetup; +export interface GetUserOptions { + security: SecurityPluginSetup | null | undefined; request: KibanaRequest; } export const getUser = ({ security, request }: GetUserOptions): string => { - const authenticatedUser = security.authc.getCurrentUser(request); - if (authenticatedUser != null) { - return authenticatedUser.username; + if (security != null) { + const authenticatedUser = security.authc.getCurrentUser(request); + if (authenticatedUser != null) { + return authenticatedUser.username; + } else { + return 'elastic'; + } } else { return 'elastic'; } diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts index 4473d68d3c646..2498c36967a53 100644 --- a/x-pack/plugins/lists/server/plugin.ts +++ b/x-pack/plugins/lists/server/plugin.ts @@ -5,7 +5,7 @@ */ import { first } from 'rxjs/operators'; -import { ElasticsearchServiceSetup, Logger, PluginInitializerContext } from 'kibana/server'; +import { Logger, PluginInitializerContext } from 'kibana/server'; import { CoreSetup } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -16,12 +16,13 @@ import { initRoutes } from './routes/init_routes'; import { ListClient } from './services/lists/client'; import { ContextProvider, ContextProviderReturn, PluginsSetup } from './types'; import { createConfig$ } from './create_config'; +import { getSpaceId } from './get_space_id'; +import { getUser } from './get_user'; export class ListPlugin { private readonly logger: Logger; private spaces: SpacesServiceSetup | undefined | null; private config: ConfigType | undefined | null; - private elasticsearch: ElasticsearchServiceSetup | undefined | null; private security: SecurityPluginSetup | undefined | null; constructor(private readonly initializerContext: PluginInitializerContext) { @@ -38,7 +39,6 @@ export class ListPlugin { ); this.spaces = plugins.spaces?.spacesService; this.config = config; - this.elasticsearch = core.elasticsearch; this.security = plugins.security; core.http.registerRouteHandlerContext('lists', this.createRouteHandlerContext()); @@ -56,28 +56,28 @@ export class ListPlugin { private createRouteHandlerContext = (): ContextProvider => { return async (context, request): ContextProviderReturn => { - const { spaces, config, security, elasticsearch } = this; + const { spaces, config, security } = this; const { core: { - elasticsearch: { dataClient }, + elasticsearch: { + dataClient: { callAsCurrentUser }, + }, }, } = context; if (config == null) { throw new TypeError('Configuration is required for this plugin to operate'); - } else if (elasticsearch == null) { - throw new TypeError('Elastic Search is required for this plugin to operate'); - } else if (security == null) { - // TODO: This might be null, test authentication being turned off. - throw new TypeError('Security plugin is required for this plugin to operate'); } else { + const spaceId = getSpaceId({ request, spaces }); + const user = getUser({ request, security }); return { getListClient: (): ListClient => new ListClient({ + callCluster: callAsCurrentUser, config, - dataClient, request, security, - spaces, + spaceId, + user, }), }; } diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts index 946e1c240be31..48deb3ee86820 100644 --- a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts +++ b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TestReadable } from '../mocks/test_readable'; +import { TestReadable } from '../mocks'; import { BufferLines } from './buffer_lines'; diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts index b2bca241c468c..abbb270149955 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts @@ -31,7 +31,7 @@ describe('crete_list_item', () => { expect(listItem).toEqual(expected); }); - test('It calls "callAsCurrentUser" with body, index, and listIndex', async () => { + test('It calls "callCluster" with body, index, and listIndex', async () => { const options = getCreateListItemOptionsMock(); await createListItem(options); const body = getIndexESListItemMock(); @@ -40,7 +40,7 @@ describe('crete_list_item', () => { id: LIST_ITEM_ID, index: LIST_ITEM_INDEX, }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('index', expected); + expect(options.callCluster).toBeCalledWith('index', expected); }); test('It returns an auto-generated id if id is sent in undefined', async () => { diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts index da1e192bf2412..83a118b795192 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -6,6 +6,7 @@ import uuid from 'uuid'; import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { IdOrUndefined, @@ -14,7 +15,6 @@ import { MetaOrUndefined, Type, } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { transformListItemToElasticQuery } from '../utils'; export interface CreateListItemOptions { @@ -22,7 +22,7 @@ export interface CreateListItemOptions { listId: string; type: Type; value: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; user: string; meta: MetaOrUndefined; @@ -35,7 +35,7 @@ export const createListItem = async ({ listId, type, value, - dataClient, + callCluster, listItemIndex, user, meta, @@ -58,7 +58,7 @@ export const createListItem = async ({ ...transformListItemToElasticQuery({ type, value }), }; - const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('index', { + const response: CreateDocumentResponse = await callCluster('index', { body, id, index: listItemIndex, diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts index 9263b975b20e7..94cc57b53b4e2 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts @@ -24,13 +24,13 @@ describe('crete_list_item_bulk', () => { jest.clearAllMocks(); }); - test('It calls "callAsCurrentUser" with body, index, and the bulk items', async () => { + test('It calls "callCluster" with body, index, and the bulk items', async () => { const options = getCreateListItemBulkOptionsMock(); await createListItemsBulk(options); const firstRecord: IndexEsListItemSchema = getIndexESListItemMock(); const secondRecord: IndexEsListItemSchema = getIndexESListItemMock(VALUE_2); [firstRecord.tie_breaker_id, secondRecord.tie_breaker_id] = TIE_BREAKERS; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('bulk', { + expect(options.callCluster).toBeCalledWith('bulk', { body: [ { create: { _index: LIST_ITEM_INDEX } }, firstRecord, @@ -44,6 +44,6 @@ describe('crete_list_item_bulk', () => { test('It should not call the dataClient when the values are empty', async () => { const options = getCreateListItemBulkOptionsMock(); options.value = []; - expect(options.dataClient.callAsCurrentUser).not.toBeCalled(); + expect(options.callCluster).not.toBeCalled(); }); }); diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts index 7100a5f8eaabc..eac294c5f244a 100644 --- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -5,9 +5,9 @@ */ import uuid from 'uuid'; +import { APICaller } from 'kibana/server'; import { transformListItemToElasticQuery } from '../utils'; -import { DataClient } from '../../types'; import { CreateEsBulkTypeSchema, IndexEsListItemSchema, @@ -19,7 +19,7 @@ export interface CreateListItemsBulkOptions { listId: string; type: Type; value: string[]; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; user: string; meta: MetaOrUndefined; @@ -31,7 +31,7 @@ export const createListItemsBulk = async ({ listId, type, value, - dataClient, + callCluster, listItemIndex, user, meta, @@ -63,7 +63,7 @@ export const createListItemsBulk = async ({ [] ); - await dataClient.callAsCurrentUser('bulk', { + await callCluster('bulk', { body, index: listItemIndex, }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts index 795c579462b69..00fcefb2c379f 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LIST_ITEM_ID, LIST_ITEM_INDEX, getListItemResponseMock } from '../mocks'; -import { getDeleteListItemOptionsMock } from '../mocks/get_delete_list_item_options_mock'; +import { + LIST_ITEM_ID, + LIST_ITEM_INDEX, + getDeleteListItemOptionsMock, + getListItemResponseMock, +} from '../mocks'; import { getListItem } from './get_list_item'; import { deleteListItem } from './delete_list_item'; @@ -37,6 +41,7 @@ describe('delete_list_item', () => { const deletedListItem = await deleteListItem(options); expect(deletedListItem).toEqual(listItem); }); + test('Delete calls "delete" if a list item is returned from "getListItem"', async () => { const listItem = getListItemResponseMock(); ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(listItem); @@ -46,6 +51,6 @@ describe('delete_list_item', () => { id: LIST_ITEM_ID, index: LIST_ITEM_INDEX, }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('delete', deleteQuery); + expect(options.callCluster).toBeCalledWith('delete', deleteQuery); }); }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts index ffce2d3b2af81..9992f43387c89 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts @@ -4,27 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'kibana/server'; + import { Id, ListItemSchema } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { getListItem } from '.'; export interface DeleteListItemOptions { id: Id; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; } export const deleteListItem = async ({ id, - dataClient, + callCluster, listItemIndex, }: DeleteListItemOptions): Promise => { - const listItem = await getListItem({ dataClient, id, listItemIndex }); + const listItem = await getListItem({ callCluster, id, listItemIndex }); if (listItem == null) { return null; } else { - await dataClient.callAsCurrentUser('delete', { + await callCluster('delete', { id, index: listItemIndex, }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts index dee890445f9a3..c7c80638e4c37 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts @@ -52,6 +52,6 @@ describe('delete_list_item_by_value', () => { }, index: '.items', }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('deleteByQuery', deleteByQuery); + expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); }); }); diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts index f2f5ec3078e62..ec29f14a0ff64 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'kibana/server'; + import { ListItemArraySchema, Type } from '../../../common/schemas'; import { getQueryFilterFromTypeValue } from '../utils'; -import { DataClient } from '../../types'; import { getListItemByValues } from './get_list_item_by_values'; @@ -14,7 +15,7 @@ export interface DeleteListItemByValueOptions { listId: string; type: Type; value: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; } @@ -22,11 +23,11 @@ export const deleteListItemByValue = async ({ listId, value, type, - dataClient, + callCluster, listItemIndex, }: DeleteListItemByValueOptions): Promise => { const listItems = await getListItemByValues({ - dataClient, + callCluster, listId, listItemIndex, type, @@ -38,7 +39,7 @@ export const deleteListItemByValue = async ({ type, value: values, }); - await dataClient.callAsCurrentUser('deleteByQuery', { + await callCluster('deleteByQuery', { body: { query: { bool: { diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts index 937993f1d8f71..31a421c2e31bf 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts @@ -4,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LIST_ID, LIST_INDEX, getDataClientMock, getListItemResponseMock } from '../mocks'; -import { getSearchListItemMock } from '../mocks/get_search_list_item_mock'; +import { + LIST_ID, + LIST_INDEX, + getCallClusterMock, + getListItemResponseMock, + getSearchListItemMock, +} from '../mocks'; import { getListItem } from './get_list_item'; @@ -20,8 +25,8 @@ describe('get_list_item', () => { test('it returns a list item as expected if the list item is found', async () => { const data = getSearchListItemMock(); - const dataClient = getDataClientMock(data); - const list = await getListItem({ dataClient, id: LIST_ID, listItemIndex: LIST_INDEX }); + const callCluster = getCallClusterMock(data); + const list = await getListItem({ callCluster, id: LIST_ID, listItemIndex: LIST_INDEX }); const expected = getListItemResponseMock(); expect(list).toEqual(expected); }); @@ -29,8 +34,8 @@ describe('get_list_item', () => { test('it returns null if the search is empty', async () => { const data = getSearchListItemMock(); data.hits.hits = []; - const dataClient = getDataClientMock(data); - const list = await getListItem({ dataClient, id: LIST_ID, listItemIndex: LIST_INDEX }); + const callCluster = getCallClusterMock(data); + const list = await getListItem({ callCluster, id: LIST_ID, listItemIndex: LIST_INDEX }); expect(list).toEqual(null); }); }); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item.ts b/x-pack/plugins/lists/server/services/items/get_list_item.ts index 1c91b69801648..83b30d336ccd4 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts @@ -5,36 +5,33 @@ */ import { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { Id, ListItemSchema, SearchEsListItemSchema } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { deriveTypeFromItem, transformElasticToListItem } from '../utils'; interface GetListItemOptions { id: Id; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; } export const getListItem = async ({ id, - dataClient, + callCluster, listItemIndex, }: GetListItemOptions): Promise => { - const listItemES: SearchResponse = await dataClient.callAsCurrentUser( - 'search', - { - body: { - query: { - term: { - _id: id, - }, + const listItemES: SearchResponse = await callCluster('search', { + body: { + query: { + term: { + _id: id, }, }, - ignoreUnavailable: true, - index: listItemIndex, - } - ); + }, + ignoreUnavailable: true, + index: listItemIndex, + }); if (listItemES.hits.hits.length) { const type = deriveTypeFromItem({ item: listItemES.hits.hits[0]._source }); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts index a6efcbc0d3ffb..49bcf12043d7c 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'kibana/server'; + import { ListItemArraySchema, Type } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { getListItemByValues } from '.'; export interface GetListItemByValueOptions { listId: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; type: Type; value: string; @@ -19,13 +20,13 @@ export interface GetListItemByValueOptions { export const getListItemByValue = async ({ listId, - dataClient, + callCluster, listItemIndex, type, value, }: GetListItemByValueOptions): Promise => getListItemByValues({ - dataClient, + callCluster, listId, listItemIndex, type, diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts index 55b170487d95a..7f5fff4dc3147 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.test.ts @@ -4,8 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2, getDataClientMock } from '../mocks'; -import { getSearchListItemMock } from '../mocks/get_search_list_item_mock'; +import { + LIST_ID, + LIST_ITEM_INDEX, + TYPE, + VALUE, + VALUE_2, + getCallClusterMock, + getSearchListItemMock, +} from '../mocks'; import { getListItemByValues } from './get_list_item_by_values'; @@ -21,27 +28,29 @@ describe('get_list_item_by_values', () => { test('Returns a an empty array if the ES query is also empty', async () => { const data = getSearchListItemMock(); data.hits.hits = []; - const dataClient = getDataClientMock(data); + const callCluster = getCallClusterMock(data); const listItem = await getListItemByValues({ - dataClient, + callCluster, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, value: [VALUE, VALUE_2], }); + expect(listItem).toEqual([]); }); test('Returns transformed list item if the data exists within ES', async () => { const data = getSearchListItemMock(); - const dataClient = getDataClientMock(data); + const callCluster = getCallClusterMock(data); const listItem = await getListItemByValues({ - dataClient, + callCluster, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, value: [VALUE, VALUE_2], }); + expect(listItem).toEqual([ { created_at: '2020-04-20T15:25:31.830Z', diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts index 1e5c0b4a6655c..29b9b01754027 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts @@ -5,14 +5,14 @@ */ import { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { getQueryFilterFromTypeValue, transformElasticToListItem } from '../utils'; export interface GetListItemByValuesOptions { listId: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; type: Type; value: string[]; @@ -20,25 +20,22 @@ export interface GetListItemByValuesOptions { export const getListItemByValues = async ({ listId, - dataClient, + callCluster, listItemIndex, type, value, }: GetListItemByValuesOptions): Promise => { - const response: SearchResponse = await dataClient.callAsCurrentUser( - 'search', - { - body: { - query: { - bool: { - filter: getQueryFilterFromTypeValue({ listId, type, value }), - }, + const response: SearchResponse = await callCluster('search', { + body: { + query: { + bool: { + filter: getQueryFilterFromTypeValue({ listId, type, value }), }, }, - ignoreUnavailable: true, - index: listItemIndex, - size: value.length, // This has a limit on the number which is 10k - } - ); + }, + ignoreUnavailable: true, + index: listItemIndex, + size: value.length, // This has a limit on the number which is 10k + }); return transformElasticToListItem({ response, type }); }; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts index 0ea8320e966bd..ffe2eff9f3ca7 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts @@ -4,17 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock } from 'src/core/server/mocks'; -import { KibanaRequest } from 'src/core/server'; - -import { getSpace } from '../utils'; - import { getListItemIndex } from './get_list_item_index'; -jest.mock('../utils', () => ({ - getSpace: jest.fn(), -})); - describe('get_list_item_index', () => { beforeEach(() => { jest.clearAllMocks(); @@ -25,13 +16,9 @@ describe('get_list_item_index', () => { }); test('Returns the list item index when there is a space', async () => { - ((getSpace as unknown) as jest.Mock).mockReturnValueOnce('test-space'); - const rawRequest = httpServerMock.createRawRequest({}); - const request = KibanaRequest.from(rawRequest); const listIndex = getListItemIndex({ listsItemsIndexName: 'lists-items-index', - request, - spaces: undefined, + spaceId: 'test-space', }); expect(listIndex).toEqual('lists-items-index-test-space'); }); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_index.ts b/x-pack/plugins/lists/server/services/items/get_list_item_index.ts index c9f1bfd4d44e4..4cd93df6d9bf4 100644 --- a/x-pack/plugins/lists/server/services/items/get_list_item_index.ts +++ b/x-pack/plugins/lists/server/services/items/get_list_item_index.ts @@ -4,19 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'kibana/server'; - -import { SpacesServiceSetup } from '../../../../spaces/server'; -import { getSpace } from '../utils'; - -interface GetListItemIndexOptions { - spaces: SpacesServiceSetup | undefined | null; - request: KibanaRequest; +export interface GetListItemIndexOptions { + spaceId: string; listsItemsIndexName: string; } export const getListItemIndex = ({ - spaces, - request, + spaceId, listsItemsIndexName, -}: GetListItemIndexOptions): string => `${listsItemsIndexName}-${getSpace({ request, spaces })}`; +}: GetListItemIndexOptions): string => `${listsItemsIndexName}-${spaceId}`; diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts index ce4f8125d77af..6a71b2a0caf41 100644 --- a/x-pack/plugins/lists/server/services/items/update_list_item.ts +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -5,6 +5,7 @@ */ import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { Id, @@ -13,14 +14,13 @@ import { UpdateEsListItemSchema, } from '../../../common/schemas'; import { transformListItemToElasticQuery } from '../utils'; -import { DataClient } from '../../types'; import { getListItem } from './get_list_item'; export interface UpdateListItemOptions { id: Id; value: string | null | undefined; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; user: string; meta: MetaOrUndefined; @@ -30,14 +30,14 @@ export interface UpdateListItemOptions { export const updateListItem = async ({ id, value, - dataClient, + callCluster, listItemIndex, user, meta, dateNow, }: UpdateListItemOptions): Promise => { const updatedAt = dateNow ?? new Date().toISOString(); - const listItem = await getListItem({ dataClient, id, listItemIndex }); + const listItem = await getListItem({ callCluster, id, listItemIndex }); if (listItem == null) { return null; } else { @@ -48,7 +48,7 @@ export const updateListItem = async ({ ...transformListItemToElasticQuery({ type: listItem.type, value: value ?? listItem.value }), }; - const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('update', { + const response: CreateDocumentResponse = await callCluster('update', { body: { doc, }, diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts index 1fe1023e28ab9..542c2bb12d8e5 100644 --- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -6,8 +6,9 @@ import { Readable } from 'stream'; +import { APICaller } from 'kibana/server'; + import { MetaOrUndefined, Type } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { BufferLines } from './buffer_lines'; import { getListItemByValues } from './get_list_item_by_values'; @@ -16,7 +17,7 @@ import { createListItemsBulk } from './create_list_items_bulk'; export interface ImportListItemsToStreamOptions { listId: string; stream: Readable; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; type: Type; user: string; @@ -26,7 +27,7 @@ export interface ImportListItemsToStreamOptions { export const importListItemsToStream = ({ listId, stream, - dataClient, + callCluster, listItemIndex, type, user, @@ -37,7 +38,7 @@ export const importListItemsToStream = ({ readBuffer.on('lines', async (lines: string[]) => { await writeBufferToItems({ buffer: lines, - dataClient, + callCluster, listId, listItemIndex, meta, @@ -54,7 +55,7 @@ export const importListItemsToStream = ({ export interface WriteBufferToItemsOptions { listId: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; buffer: string[]; type: Type; @@ -69,7 +70,7 @@ export interface LinesResult { export const writeBufferToItems = async ({ listId, - dataClient, + callCluster, listItemIndex, buffer, type, @@ -77,7 +78,7 @@ export const writeBufferToItems = async ({ meta, }: WriteBufferToItemsOptions): Promise => { const items = await getListItemByValues({ - dataClient, + callCluster, listId, listItemIndex, type, @@ -89,7 +90,7 @@ export const writeBufferToItems = async ({ const linesProcessed = duplicatesRemoved.length; const duplicatesFound = buffer.length - duplicatesRemoved.length; await createListItemsBulk({ - dataClient, + callCluster, listId, listItemIndex, meta, diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts index 63e9aeb61bad0..b08e5fa688b4b 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts @@ -7,13 +7,13 @@ import { LIST_ID, LIST_ITEM_INDEX, - getDataClientMock, + getCallClusterMock, getExportListItemsToStreamOptionsMock, getResponseOptionsMock, + getSearchListItemMock, getWriteNextResponseOptions, getWriteResponseHitsToStreamOptionsMock, } from '../mocks'; -import { getSearchListItemMock } from '../mocks/get_search_list_item_mock'; import { exportListItemsToStream, @@ -37,7 +37,7 @@ describe('write_list_items_to_stream', () => { const options = getExportListItemsToStreamOptionsMock(); const firstResponse = getSearchListItemMock(); firstResponse.hits.hits = []; - options.dataClient = getDataClientMock(firstResponse); + options.callCluster = getCallClusterMock(firstResponse); exportListItemsToStream(options); let chunks: string[] = []; @@ -71,7 +71,7 @@ describe('write_list_items_to_stream', () => { const firstResponse = getSearchListItemMock(); const secondResponse = getSearchListItemMock(); firstResponse.hits.hits = [...firstResponse.hits.hits, ...secondResponse.hits.hits]; - options.dataClient = getDataClientMock(firstResponse); + options.callCluster = getCallClusterMock(firstResponse); exportListItemsToStream(options); let chunks: string[] = []; @@ -90,15 +90,14 @@ describe('write_list_items_to_stream', () => { const firstResponse = getSearchListItemMock(); firstResponse.hits.hits[0].sort = ['some-sort-value']; + const secondResponse = getSearchListItemMock(); secondResponse.hits.hits[0]._source.ip = '255.255.255.255'; - const jestCalls = jest.fn().mockResolvedValueOnce(firstResponse); - jestCalls.mockResolvedValueOnce(secondResponse); - - const dataClient = getDataClientMock(firstResponse); - dataClient.callAsCurrentUser = jestCalls; - options.dataClient = dataClient; + options.callCluster = jest + .fn() + .mockResolvedValueOnce(firstResponse) + .mockResolvedValueOnce(secondResponse); exportListItemsToStream(options); @@ -125,7 +124,7 @@ describe('write_list_items_to_stream', () => { const listItem = getSearchListItemMock(); listItem.hits.hits[0].sort = ['sort-value-1']; const options = getWriteNextResponseOptions(); - options.dataClient = getDataClientMock(listItem); + options.callCluster = getCallClusterMock(listItem); const searchAfter = await writeNextResponse(options); expect(searchAfter).toEqual(['sort-value-1']); }); @@ -134,7 +133,7 @@ describe('write_list_items_to_stream', () => { const listItem = getSearchListItemMock(); listItem.hits.hits = []; const options = getWriteNextResponseOptions(); - options.dataClient = getDataClientMock(listItem); + options.callCluster = getCallClusterMock(listItem); const searchAfter = await writeNextResponse(options); expect(searchAfter).toEqual(undefined); }); @@ -187,7 +186,7 @@ describe('write_list_items_to_stream', () => { index: LIST_ITEM_INDEX, size: 100, }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('search', expected); + expect(options.callCluster).toBeCalledWith('search', expected); }); test('It returns a simple response with expected values and size changed', async () => { @@ -205,7 +204,7 @@ describe('write_list_items_to_stream', () => { index: LIST_ITEM_INDEX, size: 33, }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('search', expected); + expect(options.callCluster).toBeCalledWith('search', expected); }); }); diff --git a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts index 0e0ae7b924e17..b81e4a4fc73c2 100644 --- a/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts @@ -7,9 +7,9 @@ import { PassThrough } from 'stream'; import { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { SearchEsListItemSchema } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { ErrorWithStatusCode } from '../../error_with_status_code'; /** @@ -20,7 +20,7 @@ export const SIZE = 100; export interface ExportListItemsToStreamOptions { listId: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; stream: PassThrough; stringToAppend: string | null | undefined; @@ -28,7 +28,7 @@ export interface ExportListItemsToStreamOptions { export const exportListItemsToStream = ({ listId, - dataClient, + callCluster, stream, listItemIndex, stringToAppend, @@ -37,7 +37,7 @@ export const exportListItemsToStream = ({ // and prevent the async await from bubbling up to the caller setTimeout(async () => { let searchAfter = await writeNextResponse({ - dataClient, + callCluster, listId, listItemIndex, searchAfter: undefined, @@ -46,7 +46,7 @@ export const exportListItemsToStream = ({ }); while (searchAfter != null) { searchAfter = await writeNextResponse({ - dataClient, + callCluster, listId, listItemIndex, searchAfter, @@ -60,7 +60,7 @@ export const exportListItemsToStream = ({ export interface WriteNextResponseOptions { listId: string; - dataClient: DataClient; + callCluster: APICaller; listItemIndex: string; stream: PassThrough; searchAfter: string[] | undefined; @@ -69,14 +69,14 @@ export interface WriteNextResponseOptions { export const writeNextResponse = async ({ listId, - dataClient, + callCluster, stream, listItemIndex, searchAfter, stringToAppend, }: WriteNextResponseOptions): Promise => { const response = await getResponse({ - dataClient, + callCluster, listId, listItemIndex, searchAfter, @@ -100,7 +100,7 @@ export const getSearchAfterFromResponse = ({ : undefined; export interface GetResponseOptions { - dataClient: DataClient; + callCluster: APICaller; listId: string; searchAfter: undefined | string[]; listItemIndex: string; @@ -108,13 +108,13 @@ export interface GetResponseOptions { } export const getResponse = async ({ - dataClient, + callCluster, searchAfter, listId, listItemIndex, size = SIZE, }: GetResponseOptions): Promise> => { - return dataClient.callAsCurrentUser('search', { + return callCluster('search', { body: { query: { term: { diff --git a/x-pack/plugins/lists/server/services/lists/client.ts b/x-pack/plugins/lists/server/services/lists/client.ts index 32578fc739f26..ba22bf72cc18c 100644 --- a/x-pack/plugins/lists/server/services/lists/client.ts +++ b/x-pack/plugins/lists/server/services/lists/client.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, ScopedClusterClient } from 'src/core/server'; +import { APICaller } from 'kibana/server'; -import { SecurityPluginSetup } from '../../../../security/server'; -import { SpacesServiceSetup } from '../../../../spaces/server'; import { ListItemArraySchema, ListItemSchema, ListSchema } from '../../../common/schemas'; import { ConfigType } from '../../config'; import { @@ -31,7 +29,6 @@ import { importListItemsToStream, updateListItem, } from '../../services/items'; -import { getUser } from '../../services/utils'; import { createBootstrapIndex, deleteAllIndex, @@ -64,47 +61,39 @@ import { UpdateListOptions, } from './client_types'; -// TODO: Consider an interface and a factory export class ListClient { - private readonly spaces: SpacesServiceSetup | undefined | null; + private readonly spaceId: string; + private readonly user: string; private readonly config: ConfigType; - private readonly dataClient: Pick< - ScopedClusterClient, - 'callAsCurrentUser' | 'callAsInternalUser' - >; - private readonly request: KibanaRequest; - private readonly security: SecurityPluginSetup; - - constructor({ request, spaces, config, dataClient, security }: ConstructorOptions) { - this.request = request; - this.spaces = spaces; + private readonly callCluster: APICaller; + + constructor({ spaceId, user, config, callCluster }: ConstructorOptions) { + this.spaceId = spaceId; + this.user = user; this.config = config; - this.dataClient = dataClient; - this.security = security; + this.callCluster = callCluster; } public getListIndex = (): string => { const { - spaces, - request, + spaceId, config: { listIndex: listsIndexName }, } = this; - return getListIndex({ listsIndexName, request, spaces }); + return getListIndex({ listsIndexName, spaceId }); }; public getListItemIndex = (): string => { const { - spaces, - request, + spaceId, config: { listItemIndex: listsItemsIndexName }, } = this; - return getListItemIndex({ listsItemsIndexName, request, spaces }); + return getListItemIndex({ listsItemsIndexName, spaceId }); }; public getList = async ({ id }: GetListOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return getList({ dataClient, id, listIndex }); + return getList({ callCluster, id, listIndex }); }; public createList = async ({ @@ -114,10 +103,9 @@ export class ListClient { type, meta, }: CreateListOptions): Promise => { - const { dataClient, security, request } = this; + const { callCluster, user } = this; const listIndex = this.getListIndex(); - const user = getUser({ request, security }); - return createList({ dataClient, description, id, listIndex, meta, name, type, user }); + return createList({ callCluster, description, id, listIndex, meta, name, type, user }); }; public createListIfItDoesNotExist = async ({ @@ -136,67 +124,51 @@ export class ListClient { }; public getListIndexExists = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return getIndexExists(callAsCurrentUser, listIndex); + return getIndexExists(callCluster, listIndex); }; public getListItemIndexExists = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return getIndexExists(callAsCurrentUser, listItemIndex); + return getIndexExists(callCluster, listItemIndex); }; public createListBootStrapIndex = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return createBootstrapIndex(callAsCurrentUser, listIndex); + return createBootstrapIndex(callCluster, listIndex); }; public createListItemBootStrapIndex = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return createBootstrapIndex(callAsCurrentUser, listItemIndex); + return createBootstrapIndex(callCluster, listItemIndex); }; public getListPolicyExists = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return getPolicyExists(callAsCurrentUser, listIndex); + return getPolicyExists(callCluster, listIndex); }; public getListItemPolicyExists = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listsItemIndex = this.getListItemIndex(); - return getPolicyExists(callAsCurrentUser, listsItemIndex); + return getPolicyExists(callCluster, listsItemIndex); }; public getListTemplateExists = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return getTemplateExists(callAsCurrentUser, listIndex); + return getTemplateExists(callCluster, listIndex); }; public getListItemTemplateExists = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return getTemplateExists(callAsCurrentUser, listItemIndex); + return getTemplateExists(callCluster, listItemIndex); }; public getListTemplate = (): Record => { @@ -210,91 +182,71 @@ export class ListClient { }; public setListTemplate = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const template = this.getListTemplate(); const listIndex = this.getListIndex(); - return setTemplate(callAsCurrentUser, listIndex, template); + return setTemplate(callCluster, listIndex, template); }; public setListItemTemplate = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const template = this.getListItemTemplate(); const listItemIndex = this.getListItemIndex(); - return setTemplate(callAsCurrentUser, listItemIndex, template); + return setTemplate(callCluster, listItemIndex, template); }; public setListPolicy = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return setPolicy(callAsCurrentUser, listIndex, listPolicy); + return setPolicy(callCluster, listIndex, listPolicy); }; public setListItemPolicy = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return setPolicy(callAsCurrentUser, listItemIndex, listsItemsPolicy); + return setPolicy(callCluster, listItemIndex, listsItemsPolicy); }; public deleteListIndex = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return deleteAllIndex(callAsCurrentUser, `${listIndex}-*`); + return deleteAllIndex(callCluster, `${listIndex}-*`); }; public deleteListItemIndex = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return deleteAllIndex(callAsCurrentUser, `${listItemIndex}-*`); + return deleteAllIndex(callCluster, `${listItemIndex}-*`); }; public deleteListPolicy = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return deletePolicy(callAsCurrentUser, listIndex); + return deletePolicy(callCluster, listIndex); }; public deleteListItemPolicy = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return deletePolicy(callAsCurrentUser, listItemIndex); + return deletePolicy(callCluster, listItemIndex); }; public deleteListTemplate = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); - return deleteTemplate(callAsCurrentUser, listIndex); + return deleteTemplate(callCluster, listIndex); }; public deleteListItemTemplate = async (): Promise => { - const { - dataClient: { callAsCurrentUser }, - } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return deleteTemplate(callAsCurrentUser, listItemIndex); + return deleteTemplate(callCluster, listItemIndex); }; public deleteListItem = async ({ id }: DeleteListItemOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); - return deleteListItem({ dataClient, id, listItemIndex }); + return deleteListItem({ callCluster, id, listItemIndex }); }; public deleteListItemByValue = async ({ @@ -302,10 +254,10 @@ export class ListClient { value, type, }: DeleteListItemByValueOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); return deleteListItemByValue({ - dataClient, + callCluster, listId, listItemIndex, type, @@ -314,11 +266,11 @@ export class ListClient { }; public deleteList = async ({ id }: DeleteListOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listIndex = this.getListIndex(); const listItemIndex = this.getListItemIndex(); return deleteList({ - dataClient, + callCluster, id, listIndex, listItemIndex, @@ -330,10 +282,10 @@ export class ListClient { listId, stream, }: ExportListItemsToStreamOptions): void => { - const { dataClient } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); exportListItemsToStream({ - dataClient, + callCluster, listId, listItemIndex, stream, @@ -347,11 +299,10 @@ export class ListClient { stream, meta, }: ImportListItemsToStreamOptions): Promise => { - const { dataClient, security, request } = this; + const { callCluster, user } = this; const listItemIndex = this.getListItemIndex(); - const user = getUser({ request, security }); return importListItemsToStream({ - dataClient, + callCluster, listId, listItemIndex, meta, @@ -366,10 +317,10 @@ export class ListClient { value, type, }: GetListItemByValueOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); return getListItemByValue({ - dataClient, + callCluster, listId, listItemIndex, type, @@ -384,11 +335,10 @@ export class ListClient { type, meta, }: CreateListItemOptions): Promise => { - const { dataClient, security, request } = this; + const { callCluster, user } = this; const listItemIndex = this.getListItemIndex(); - const user = getUser({ request, security }); return createListItem({ - dataClient, + callCluster, id, listId, listItemIndex, @@ -404,11 +354,10 @@ export class ListClient { value, meta, }: UpdateListItemOptions): Promise => { - const { dataClient, security, request } = this; - const user = getUser({ request, security }); + const { callCluster, user } = this; const listItemIndex = this.getListItemIndex(); return updateListItem({ - dataClient, + callCluster, id, listItemIndex, meta, @@ -423,11 +372,10 @@ export class ListClient { description, meta, }: UpdateListOptions): Promise => { - const { dataClient, security, request } = this; - const user = getUser({ request, security }); + const { callCluster, user } = this; const listIndex = this.getListIndex(); return updateList({ - dataClient, + callCluster, description, id, listIndex, @@ -438,10 +386,10 @@ export class ListClient { }; public getListItem = async ({ id }: GetListItemOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); return getListItem({ - dataClient, + callCluster, id, listItemIndex, }); @@ -452,10 +400,10 @@ export class ListClient { listId, value, }: GetListItemsByValueOptions): Promise => { - const { dataClient } = this; + const { callCluster } = this; const listItemIndex = this.getListItemIndex(); return getListItemByValues({ - dataClient, + callCluster, listId, listItemIndex, type, diff --git a/x-pack/plugins/lists/server/services/lists/client_types.ts b/x-pack/plugins/lists/server/services/lists/client_types.ts index c3b6a484d8787..2cc58c02dbfcf 100644 --- a/x-pack/plugins/lists/server/services/lists/client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/client_types.ts @@ -6,10 +6,9 @@ import { PassThrough, Readable } from 'stream'; -import { KibanaRequest } from 'kibana/server'; +import { APICaller, KibanaRequest } from 'kibana/server'; import { SecurityPluginSetup } from '../../../../security/server'; -import { SpacesServiceSetup } from '../../../../spaces/server'; import { Description, DescriptionOrUndefined, @@ -21,14 +20,14 @@ import { Type, } from '../../../common/schemas'; import { ConfigType } from '../../config'; -import { DataClient } from '../../types'; export interface ConstructorOptions { + callCluster: APICaller; config: ConfigType; - dataClient: DataClient; request: KibanaRequest; - spaces: SpacesServiceSetup | undefined | null; - security: SecurityPluginSetup; + spaceId: string; + user: string; + security: SecurityPluginSetup | undefined | null; } export interface GetListOptions { diff --git a/x-pack/plugins/lists/server/services/lists/create_list.test.ts b/x-pack/plugins/lists/server/services/lists/create_list.test.ts index d6ba435155c60..36284a70fb97d 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts @@ -31,7 +31,7 @@ describe('crete_list', () => { expect(list).toEqual(expected); }); - test('It calls "callAsCurrentUser" with body, index, and listIndex', async () => { + test('It calls "callCluster" with body, index, and listIndex', async () => { const options = getCreateListOptionsMock(); await createList(options); const body = getIndexESListMock(); @@ -40,7 +40,7 @@ describe('crete_list', () => { id: LIST_ID, index: LIST_INDEX, }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('index', expected); + expect(options.callCluster).toBeCalledWith('index', expected); }); test('It returns an auto-generated id if id is sent in undefined', async () => { diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts index dcf87b3ad1ef1..ddbc99c88a877 100644 --- a/x-pack/plugins/lists/server/services/lists/create_list.ts +++ b/x-pack/plugins/lists/server/services/lists/create_list.ts @@ -6,8 +6,8 @@ import uuid from 'uuid'; import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; -import { DataClient } from '../../types'; import { Description, IdOrUndefined, @@ -23,7 +23,7 @@ export interface CreateListOptions { type: Type; name: Name; description: Description; - dataClient: DataClient; + callCluster: APICaller; listIndex: string; user: string; meta: MetaOrUndefined; @@ -36,7 +36,7 @@ export const createList = async ({ name, type, description, - dataClient, + callCluster, listIndex, user, meta, @@ -55,7 +55,7 @@ export const createList = async ({ updated_at: createdAt, updated_by: user, }; - const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('index', { + const response: CreateDocumentResponse = await callCluster('index', { body, id, index: listIndex, diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts index f32273e3e7f76..62b5e7c7aec4a 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts @@ -52,7 +52,7 @@ describe('delete_list', () => { body: { query: { term: { list_id: LIST_ID } } }, index: LIST_ITEM_INDEX, }; - expect(options.dataClient.callAsCurrentUser).toBeCalledWith('deleteByQuery', deleteByQuery); + expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); }); test('Delete calls "delete" second if a list is returned from getList', async () => { @@ -64,13 +64,13 @@ describe('delete_list', () => { id: LIST_ID, index: LIST_INDEX, }; - expect(options.dataClient.callAsCurrentUser).toHaveBeenNthCalledWith(2, 'delete', deleteQuery); + expect(options.callCluster).toHaveBeenNthCalledWith(2, 'delete', deleteQuery); }); test('Delete does not call data client if the list returns null', async () => { ((getList as unknown) as jest.Mock).mockResolvedValueOnce(null); const options = getDeleteListOptionsMock(); await deleteList(options); - expect(options.dataClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect(options.callCluster).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts index 653a8da74a105..bc66c88b082a3 100644 --- a/x-pack/plugins/lists/server/services/lists/delete_list.ts +++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts @@ -4,29 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'kibana/server'; + import { Id, ListSchema } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { getList } from './get_list'; export interface DeleteListOptions { id: Id; - dataClient: DataClient; + callCluster: APICaller; listIndex: string; listItemIndex: string; } export const deleteList = async ({ id, - dataClient, + callCluster, listIndex, listItemIndex, }: DeleteListOptions): Promise => { - const list = await getList({ dataClient, id, listIndex }); + const list = await getList({ callCluster, id, listIndex }); if (list == null) { return null; } else { - await dataClient.callAsCurrentUser('deleteByQuery', { + await callCluster('deleteByQuery', { body: { query: { term: { @@ -37,7 +38,7 @@ export const deleteList = async ({ index: listItemIndex, }); - await dataClient.callAsCurrentUser('delete', { + await callCluster('delete', { id, index: listIndex, }); diff --git a/x-pack/plugins/lists/server/services/lists/get_list.test.ts b/x-pack/plugins/lists/server/services/lists/get_list.test.ts index 1f9a33c191764..c997d5325296a 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.test.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.test.ts @@ -7,7 +7,7 @@ import { LIST_ID, LIST_INDEX, - getDataClientMock, + getCallClusterMock, getListResponseMock, getSearchListMock, } from '../mocks'; @@ -25,8 +25,8 @@ describe('get_list', () => { test('it returns a list as expected if the list is found', async () => { const data = getSearchListMock(); - const dataClient = getDataClientMock(data); - const list = await getList({ dataClient, id: LIST_ID, listIndex: LIST_INDEX }); + const callCluster = getCallClusterMock(data); + const list = await getList({ callCluster, id: LIST_ID, listIndex: LIST_INDEX }); const expected = getListResponseMock(); expect(list).toEqual(expected); }); @@ -34,8 +34,8 @@ describe('get_list', () => { test('it returns null if the search is empty', async () => { const data = getSearchListMock(); data.hits.hits = []; - const dataClient = getDataClientMock(data); - const list = await getList({ dataClient, id: LIST_ID, listIndex: LIST_INDEX }); + const callCluster = getCallClusterMock(data); + const list = await getList({ callCluster, id: LIST_ID, listIndex: LIST_INDEX }); expect(list).toEqual(null); }); }); diff --git a/x-pack/plugins/lists/server/services/lists/get_list.ts b/x-pack/plugins/lists/server/services/lists/get_list.ts index 216703f08f069..c04bd504ad8c0 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list.ts @@ -5,22 +5,22 @@ */ import { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { Id, ListSchema, SearchEsListSchema } from '../../../common/schemas'; -import { DataClient } from '../../types'; interface GetListOptions { id: Id; - dataClient: DataClient; + callCluster: APICaller; listIndex: string; } export const getList = async ({ id, - dataClient, + callCluster, listIndex, }: GetListOptions): Promise => { - const result: SearchResponse = await dataClient.callAsCurrentUser('search', { + const result: SearchResponse = await callCluster('search', { body: { query: { term: { diff --git a/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts b/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts index 22a738a340b25..f82928ffeddd2 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts @@ -4,17 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock } from 'src/core/server/mocks'; -import { KibanaRequest } from 'src/core/server'; - -import { getSpace } from '../utils'; - import { getListIndex } from './get_list_index'; -jest.mock('../utils', () => ({ - getSpace: jest.fn(), -})); - describe('get_list_index', () => { beforeEach(() => { jest.clearAllMocks(); @@ -25,13 +16,9 @@ describe('get_list_index', () => { }); test('Returns the list index when there is a space', async () => { - ((getSpace as unknown) as jest.Mock).mockReturnValueOnce('test-space'); - const rawRequest = httpServerMock.createRawRequest({}); - const request = KibanaRequest.from(rawRequest); const listIndex = getListIndex({ listsIndexName: 'lists-index', - request, - spaces: undefined, + spaceId: 'test-space', }); expect(listIndex).toEqual('lists-index-test-space'); }); diff --git a/x-pack/plugins/lists/server/services/lists/get_list_index.ts b/x-pack/plugins/lists/server/services/lists/get_list_index.ts index 70b85fc97ebfa..5086603fa8403 100644 --- a/x-pack/plugins/lists/server/services/lists/get_list_index.ts +++ b/x-pack/plugins/lists/server/services/lists/get_list_index.ts @@ -4,16 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest } from 'kibana/server'; - -import { SpacesServiceSetup } from '../../../../spaces/server'; -import { getSpace } from '../utils'; - interface GetListIndexOptions { - spaces: SpacesServiceSetup | undefined | null; - request: KibanaRequest; + spaceId: string; listsIndexName: string; } -export const getListIndex = ({ spaces, request, listsIndexName }: GetListIndexOptions): string => - `${listsIndexName}-${getSpace({ request, spaces })}`; +export const getListIndex = ({ spaceId, listsIndexName }: GetListIndexOptions): string => + `${listsIndexName}-${spaceId}`; diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts index 55f110e9a8291..9859adf062485 100644 --- a/x-pack/plugins/lists/server/services/lists/update_list.ts +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -5,6 +5,7 @@ */ import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { DescriptionOrUndefined, @@ -14,13 +15,12 @@ import { NameOrUndefined, UpdateEsListSchema, } from '../../../common/schemas'; -import { DataClient } from '../../types'; import { getList } from '.'; export interface UpdateListOptions { id: Id; - dataClient: DataClient; + callCluster: APICaller; listIndex: string; user: string; name: NameOrUndefined; @@ -33,14 +33,14 @@ export const updateList = async ({ id, name, description, - dataClient, + callCluster, listIndex, user, meta, dateNow, }: UpdateListOptions): Promise => { const updatedAt = dateNow ?? new Date().toISOString(); - const list = await getList({ dataClient, id, listIndex }); + const list = await getList({ callCluster, id, listIndex }); if (list == null) { return null; } else { @@ -51,7 +51,7 @@ export const updateList = async ({ updated_at: updatedAt, updated_by: user, }; - const response: CreateDocumentResponse = await dataClient.callAsCurrentUser('update', { + const response: CreateDocumentResponse = await callCluster('update', { body: { doc }, id, index: listIndex, diff --git a/x-pack/plugins/lists/server/services/mocks/get_data_client_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts similarity index 57% rename from x-pack/plugins/lists/server/services/mocks/get_data_client_mock.ts rename to x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts index 6e4cc40efeed7..180ecbb797339 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_data_client_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts @@ -5,15 +5,11 @@ */ import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; import { LIST_INDEX } from './lists_services_mock_constants'; import { getShardMock } from './get_shard_mock'; -interface DataClientReturn { - callAsCurrentUser: () => Promise; - callAsInternalUser: () => Promise; -} - export const getEmptyCreateDocumentResponseMock = (): CreateDocumentResponse => ({ _id: 'elastic-id-123', _index: LIST_INDEX, @@ -24,11 +20,6 @@ export const getEmptyCreateDocumentResponseMock = (): CreateDocumentResponse => result: '', }); -export const getDataClientMock = ( - callAsCurrentUserData: unknown = getEmptyCreateDocumentResponseMock() -): DataClientReturn => ({ - callAsCurrentUser: jest.fn().mockResolvedValue(callAsCurrentUserData), - callAsInternalUser: (): Promise => { - throw new Error('This function should not be calling "callAsInternalUser"'); - }, -}); +export const getCallClusterMock = ( + callCluster: unknown = getEmptyCreateDocumentResponseMock() +): APICaller => jest.fn().mockResolvedValue(callCluster); diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts index 0f4d92cabaa7a..fcdad66d65251 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts @@ -6,7 +6,7 @@ import { CreateListItemsBulkOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, LIST_ID, @@ -20,7 +20,7 @@ import { } from './lists_services_mock_constants'; export const getCreateListItemBulkOptionsMock = (): CreateListItemsBulkOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), dateNow: DATE_NOW, listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts index 960db293f1124..17e3ad2f8de08 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts @@ -6,7 +6,7 @@ import { CreateListItemOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, LIST_ID, @@ -19,7 +19,7 @@ import { } from './lists_services_mock_constants'; export const getCreateListItemOptionsMock = (): CreateListItemOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), dateNow: DATE_NOW, id: LIST_ITEM_ID, listId: LIST_ID, diff --git a/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts index 1a005a76547f5..0ea6533fc122a 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts @@ -6,7 +6,7 @@ import { CreateListOptions } from '../lists'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, DESCRIPTION, @@ -20,7 +20,7 @@ import { } from './lists_services_mock_constants'; export const getCreateListOptionsMock = (): CreateListOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), dateNow: DATE_NOW, description: DESCRIPTION, id: LIST_ID, diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts index 58fd319589ea3..f6859e72d71b3 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts @@ -6,11 +6,11 @@ import { DeleteListItemByValueOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants'; export const getDeleteListItemByValueOptionsMock = (): DeleteListItemByValueOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts index 1e7167547a6de..271c185860b07 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts @@ -6,11 +6,11 @@ import { DeleteListItemOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ITEM_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants'; export const getDeleteListItemOptionsMock = (): DeleteListItemOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), id: LIST_ITEM_ID, listItemIndex: LIST_ITEM_INDEX, }); diff --git a/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts index 9d70dae969362..8ec92dfa4ef77 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.ts @@ -6,11 +6,11 @@ import { DeleteListOptions } from '../lists'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ID, LIST_INDEX, LIST_ITEM_INDEX } from './lists_services_mock_constants'; export const getDeleteListOptionsMock = (): DeleteListOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), id: LIST_ID, listIndex: LIST_INDEX, listItemIndex: LIST_ITEM_INDEX, diff --git a/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts index 4cc6d85cd947a..d7541f3e09e6c 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts @@ -5,12 +5,12 @@ */ import { ImportListItemsToStreamOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from './lists_services_mock_constants'; import { TestReadable } from './test_readable'; export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStreamOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, meta: META, diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts index ab1bde48e7ebf..96bc22ca7e6f2 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts @@ -6,11 +6,11 @@ import { GetListItemByValueOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants'; export const getListItemByValueOptionsMocks = (): GetListItemByValueOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts index c15d417d10289..f21f97dc8d15f 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts @@ -6,11 +6,11 @@ import { GetListItemByValuesOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from './lists_services_mock_constants'; export const getListItemByValuesOptionsMocks = (): GetListItemByValuesOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, type: TYPE, diff --git a/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts index b60d6f5113e06..0555997941baa 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts @@ -5,7 +5,7 @@ */ import { UpdateListItemOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, LIST_ITEM_ID, @@ -16,7 +16,7 @@ import { } from './lists_services_mock_constants'; export const getUpdateListItemOptionsMock = (): UpdateListItemOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), dateNow: DATE_NOW, id: LIST_ITEM_ID, listItemIndex: LIST_ITEM_INDEX, diff --git a/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts index e56ebc24bdae1..fe6fc37eaf81e 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts @@ -5,7 +5,7 @@ */ import { UpdateListOptions } from '../lists'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { DATE_NOW, DESCRIPTION, @@ -17,7 +17,7 @@ import { } from './lists_services_mock_constants'; export const getUpdateListOptionsMock = (): UpdateListOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), dateNow: DATE_NOW, description: DESCRIPTION, id: LIST_ID, diff --git a/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts index 9a77453b65d6a..d6b7d70c1aa77 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts @@ -5,12 +5,12 @@ */ import { WriteBufferToItemsOptions } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from './lists_services_mock_constants'; export const getWriteBufferToItemsOptionsMock = (): WriteBufferToItemsOptions => ({ buffer: [], - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, meta: META, diff --git a/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts index 96724c2a88045..c945818a83e8a 100644 --- a/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts +++ b/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts @@ -13,12 +13,12 @@ import { WriteResponseHitsToStreamOptions, } from '../items'; -import { getDataClientMock } from './get_data_client_mock'; import { LIST_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants'; import { getSearchListItemMock } from './get_search_list_item_mock'; +import { getCallClusterMock } from './get_call_cluster_mock'; export const getExportListItemsToStreamOptionsMock = (): ExportListItemsToStreamOptions => ({ - dataClient: getDataClientMock(getSearchListItemMock()), + callCluster: getCallClusterMock(getSearchListItemMock()), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, stream: new Stream.PassThrough(), @@ -26,7 +26,7 @@ export const getExportListItemsToStreamOptionsMock = (): ExportListItemsToStream }); export const getWriteNextResponseOptions = (): WriteNextResponseOptions => ({ - dataClient: getDataClientMock(getSearchListItemMock()), + callCluster: getCallClusterMock(getSearchListItemMock()), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, searchAfter: [], @@ -35,7 +35,7 @@ export const getWriteNextResponseOptions = (): WriteNextResponseOptions => ({ }); export const getResponseOptionsMock = (): GetResponseOptions => ({ - dataClient: getDataClientMock(), + callCluster: getCallClusterMock(), listId: LIST_ID, listItemIndex: LIST_ITEM_INDEX, searchAfter: [], diff --git a/x-pack/plugins/lists/server/services/mocks/index.ts b/x-pack/plugins/lists/server/services/mocks/index.ts index 516264149fac7..c555ba322fa2b 100644 --- a/x-pack/plugins/lists/server/services/mocks/index.ts +++ b/x-pack/plugins/lists/server/services/mocks/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './get_data_client_mock'; +export * from './get_call_cluster_mock'; export * from './get_delete_list_options_mock'; export * from './get_create_list_options_mock'; export * from './get_list_response_mock'; @@ -27,3 +27,5 @@ export * from './get_update_list_item_options_mock'; export * from './get_write_buffer_to_items_options_mock'; export * from './get_import_list_items_to_stream_options_mock'; export * from './get_write_list_items_to_stream_options_mock'; +export * from './get_search_list_item_mock'; +export * from './test_readable'; diff --git a/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts index 6e5dca7d54e5b..3b6f58479a2f2 100644 --- a/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts +++ b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.test.ts @@ -10,6 +10,14 @@ import { Type } from '../../../common/schemas'; import { deriveTypeFromItem } from './derive_type_from_es_type'; describe('derive_type_from_es_type', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + test('it returns the item ip if it exists', () => { const item = getSearchEsListItemMock(); const derivedType = deriveTypeFromItem({ item }); diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts new file mode 100644 index 0000000000000..3d48e44e26eaa --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts @@ -0,0 +1,92 @@ +/* + * 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 { QueryFilterType, getQueryFilterFromTypeValue } from './get_query_filter_from_type_value'; + +describe('get_query_filter_from_type_value', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns an ip if given an ip', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: ['127.0.0.1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { ip: ['127.0.0.1'] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns two ip if given two ip', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: ['127.0.0.1', '127.0.0.2'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { ip: ['127.0.0.1', '127.0.0.2'] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns a keyword if given a keyword', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'keyword', + value: ['host-name-1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { keyword: ['host-name-1'] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns two keywords if given two values', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'keyword', + value: ['host-name-1', 'host-name-2'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { keyword: ['host-name-1', 'host-name-2'] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns an empty keyword given an empty value', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'keyword', + value: [], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { terms: { keyword: [] } }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it returns an empty ip given an empty value', () => { + const queryFilter = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: [], + }); + const expected: QueryFilterType = [{ term: { list_id: 'list-123' } }, { terms: { ip: [] } }]; + expect(queryFilter).toEqual(expected); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/index.ts b/x-pack/plugins/lists/server/services/utils/index.ts index f256b6cb8f2d5..8a44b5ab607bf 100644 --- a/x-pack/plugins/lists/server/services/utils/index.ts +++ b/x-pack/plugins/lists/server/services/utils/index.ts @@ -7,6 +7,4 @@ export * from './get_query_filter_from_type_value'; export * from './transform_elastic_to_list_item'; export * from './transform_list_item_to_elastic_query'; -export * from './get_user'; export * from './derive_type_from_es_type'; -export * from './get_space'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts new file mode 100644 index 0000000000000..3b9864be6df53 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { ListItemArraySchema } from '../../../common/schemas'; +import { getListItemResponseMock, getSearchListItemMock } from '../mocks'; + +import { transformElasticToListItem } from './transform_elastic_to_list_item'; + +describe('transform_elastic_to_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it transforms an elastic type to a list item type', () => { + const response = getSearchListItemMock(); + const queryFilter = transformElasticToListItem({ + response, + type: 'ip', + }); + const expected: ListItemArraySchema = [getListItemResponseMock()]; + expect(queryFilter).toEqual(expected); + }); + + test('it transforms an elastic keyword type to a list item type', () => { + const response = getSearchListItemMock(); + response.hits.hits[0]._source.ip = undefined; + response.hits.hits[0]._source.keyword = 'host-name-example'; + const queryFilter = transformElasticToListItem({ + response, + type: 'keyword', + }); + const listItemResponse = getListItemResponseMock(); + listItemResponse.type = 'keyword'; + listItemResponse.value = 'host-name-example'; + const expected: ListItemArraySchema = [listItemResponse]; + expect(queryFilter).toEqual(expected); + }); + + test('it does a throw if it cannot determine the list item type from "ip"', () => { + const response = getSearchListItemMock(); + response.hits.hits[0]._source.ip = undefined; + response.hits.hits[0]._source.keyword = 'host-name-example'; + expect(() => + transformElasticToListItem({ + response, + type: 'ip', + }) + ).toThrow('Was expecting ip to not be null/undefined'); + }); + + test('it does a throw if it cannot determine the list item type from "keyword"', () => { + const response = getSearchListItemMock(); + response.hits.hits[0]._source.ip = '127.0.0.1'; + response.hits.hits[0]._source.keyword = undefined; + expect(() => + transformElasticToListItem({ + response, + type: 'keyword', + }) + ).toThrow('Was expecting keyword to not be null/undefined'); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts index 9cf673081d320..2dc0f4fe7a821 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -9,13 +9,15 @@ import { SearchResponse } from 'elasticsearch'; import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; import { ErrorWithStatusCode } from '../../error_with_status_code'; +export interface TransformElasticToListItemOptions { + response: SearchResponse; + type: Type; +} + export const transformElasticToListItem = ({ response, type, -}: { - response: SearchResponse; - type: Type; -}): ListItemArraySchema => { +}: TransformElasticToListItemOptions): ListItemArraySchema => { return response.hits.hits.map(hit => { const { _id, @@ -64,11 +66,10 @@ export const transformElasticToListItem = ({ }; } } - // TypeScript is not happy unless I have this line here return assertUnreachable(); }); }; -export const assertUnreachable = (): never => { +const assertUnreachable = (): never => { throw new Error('Unknown type in elastic_to_list_items'); }; diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts new file mode 100644 index 0000000000000..217cad30bfdbb --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { EsDataTypeUnion, Type } from '../../../common/schemas'; + +import { transformListItemToElasticQuery } from './transform_list_item_to_elastic_query'; + +describe('transform_elastic_to_elastic_query', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it transforms a ip type and value to a union', () => { + const elasticQuery = transformListItemToElasticQuery({ + type: 'ip', + value: '127.0.0.1', + }); + const expected: EsDataTypeUnion = { ip: '127.0.0.1' }; + expect(elasticQuery).toEqual(expected); + }); + + test('it transforms a keyword type and value to a union', () => { + const elasticQuery = transformListItemToElasticQuery({ + type: 'keyword', + value: 'host-name', + }); + const expected: EsDataTypeUnion = { keyword: 'host-name' }; + expect(elasticQuery).toEqual(expected); + }); + + test('it throws if the type is not known', () => { + const type: Type = 'made-up' as Type; + expect(() => + transformListItemToElasticQuery({ + type, + value: 'some-value', + }) + ).toThrow('Unknown type: "made-up" in transformListItemToElasticQuery'); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts index e68851dc3582b..051802cc41b5b 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts @@ -12,8 +12,6 @@ export const transformListItemToElasticQuery = ({ }: { type: Type; value: string; - // We disable the consistent return since we want to use typescript for exhaustive type checks - // eslint-disable-next-line consistent-return }): EsDataTypeUnion => { switch (type) { case 'ip': { @@ -27,4 +25,9 @@ export const transformListItemToElasticQuery = ({ }; } } + return assertUnreachable(type); +}; + +const assertUnreachable = (type: string): never => { + throw new Error(`Unknown type: "${type}" in transformListItemToElasticQuery`); }; diff --git a/x-pack/plugins/lists/server/types.ts b/x-pack/plugins/lists/server/types.ts index 7d509c4e27167..e0e4495d47c34 100644 --- a/x-pack/plugins/lists/server/types.ts +++ b/x-pack/plugins/lists/server/types.ts @@ -4,18 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IContextProvider, RequestHandler, ScopedClusterClient } from 'kibana/server'; +import { IContextProvider, RequestHandler } from 'kibana/server'; import { SecurityPluginSetup } from '../../security/server'; import { SpacesPluginSetup } from '../../spaces/server'; import { ListClient } from './services/lists/client'; -export type DataClient = Pick; export type ContextProvider = IContextProvider, 'lists'>; export interface PluginsSetup { - security: SecurityPluginSetup; + security: SecurityPluginSetup | undefined | null; spaces: SpacesPluginSetup | undefined | null; } From 4e58d7ae0a54023e9fbdebc5ede1252e2ee71164 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 29 Apr 2020 22:03:41 -0400 Subject: [PATCH 020/122] [Lens] Use a size of 5 for first string field in visualization (#64726) --- .../dimension_panel/dimension_panel.test.tsx | 38 +++++++++++++++++++ .../indexpattern_suggestions.test.tsx | 5 ++- .../indexpattern_suggestions.ts | 8 ++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 074c40759f8d8..9df79aa9e0908 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -1380,5 +1380,43 @@ describe('IndexPatternDimensionEditorPanel', () => { }, }); }); + + it('does not set the size of the terms aggregation', () => { + const dragging = { + field: { type: 'string', name: 'mystring', aggregatable: true }, + indexPatternId: 'foo', + }; + const testState = dragDropState(); + onDrop({ + ...defaultProps, + dragDropContext: { + ...dragDropContext, + dragging, + }, + droppedItem: dragging, + state: testState, + columnId: 'col2', + filterOperations: (op: OperationMetadata) => op.isBucketed, + layerId: 'myLayer', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col1', 'col2'], + columns: { + ...testState.layers.myLayer.columns, + col2: expect.objectContaining({ + operationType: 'terms', + params: expect.objectContaining({ size: 3 }), + }), + }, + }, + }, + }); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 2008b326a539c..02471b935c97c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -184,6 +184,7 @@ describe('IndexPattern Data Source suggestions', () => { id2: expect.objectContaining({ operationType: 'terms', sourceField: 'source', + params: expect.objectContaining({ size: 5 }), }), id3: expect.objectContaining({ operationType: 'count', @@ -388,6 +389,7 @@ describe('IndexPattern Data Source suggestions', () => { id1: expect.objectContaining({ operationType: 'terms', sourceField: 'source', + params: expect.objectContaining({ size: 5 }), }), id2: expect.objectContaining({ operationType: 'count', @@ -779,7 +781,7 @@ describe('IndexPattern Data Source suggestions', () => { expect(suggestions[0].table.columns[0].operation.isBucketed).toBeFalsy(); }); - it('appends a terms column on string field', () => { + it('appends a terms column with default size on string field', () => { const initialState = stateWithNonEmptyTables(); const suggestions = getDatasourceSuggestionsForField(initialState, '1', { name: 'dest', @@ -800,6 +802,7 @@ describe('IndexPattern Data Source suggestions', () => { id1: expect.objectContaining({ operationType: 'terms', sourceField: 'dest', + params: expect.objectContaining({ size: 3 }), }), }, }), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 2b3e976a77ea7..44963722f8afc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -17,6 +17,7 @@ import { OperationType, } from './operations'; import { operationDefinitions } from './operations/definitions'; +import { TermsIndexPatternColumn } from './operations/definitions/terms'; import { hasField } from './utils'; import { IndexPattern, @@ -232,6 +233,10 @@ function addFieldAsBucketOperation( [newColumnId]: newColumn, }; + if (buckets.length === 0 && operation === 'terms') { + (newColumn as TermsIndexPatternColumn).params.size = 5; + } + const oldDateHistogramIndex = layer.columnOrder.findIndex( columnId => layer.columns[columnId].operationType === 'date_histogram' ); @@ -327,6 +332,9 @@ function createNewLayerWithBucketAggregation( field, suggestedPriority: undefined, }); + if (operation === 'terms') { + (column as TermsIndexPatternColumn).params.size = 5; + } return { indexPatternId: indexPattern.id, From f85b3898f618d7f62178eb0772e4c2aee0f24335 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 30 Apr 2020 00:27:51 -0400 Subject: [PATCH 021/122] [Event Log] add rel=primary to saved objects for query targets (#64615) resolves https://github.com/elastic/kibana/issues/62668 Adds a property named `rel` to the nested saved objects in the event documents, whose value should not be set, or set to `primary`. The query by saved object function changes to only match event documents with that saved objects if it has the `rel: primary` value. This is used to limit searching alerting's executeAction event document with only the alert saved object, and not the action saved object (this document has an alert and action saved object). The alert saved object has the `rel: primary` field set, and the action does not. Previously, those documents were returned with a query of the action saved object. --- .../actions/server/lib/action_executor.ts | 13 +++++++-- .../create_execution_handler.test.ts | 1 + .../task_runner/create_execution_handler.ts | 4 +-- .../server/task_runner/task_runner.test.ts | 7 +++++ .../server/task_runner/task_runner.ts | 22 ++++++++++++-- .../plugins/event_log/generated/mappings.json | 4 +++ x-pack/plugins/event_log/generated/schemas.ts | 1 + x-pack/plugins/event_log/scripts/mappings.js | 6 ++++ .../server/es/cluster_client_adapter.test.ts | 21 ++++++++++++++ .../server/es/cluster_client_adapter.ts | 9 +++++- .../event_log/server/event_logger.test.ts | 29 +++++++++++++++++++ .../plugins/event_log/server/event_logger.ts | 15 +++++++++- x-pack/plugins/event_log/server/index.ts | 8 ++++- x-pack/plugins/event_log/server/types.ts | 2 ++ .../plugins/event_log/server/init_routes.ts | 2 +- .../event_log/public_api_integration.ts | 1 + .../event_log/service_api_integration.ts | 2 +- 17 files changed, 135 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 101e18f2583e3..3e9262c05efac 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -17,7 +17,7 @@ import { import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; -import { IEvent, IEventLogger } from '../../../event_log/server'; +import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; export interface ActionExecutorContext { logger: Logger; @@ -110,7 +110,16 @@ export class ActionExecutor { const actionLabel = `${actionTypeId}:${actionId}: ${name}`; const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.execute }, - kibana: { saved_objects: [{ type: 'action', id: actionId, ...namespace }] }, + kibana: { + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'action', + id: actionId, + ...namespace, + }, + ], + }, }; eventLogger.startTiming(event); diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 0e46ef4919626..a564b87f2ca50 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -95,6 +95,7 @@ test('calls actionsPlugin.execute per selected action', async () => { "saved_objects": Array [ Object { "id": "1", + "rel": "primary", "type": "alert", }, Object { diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 5c3e36b88879d..16fadc8b06cd5 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -9,7 +9,7 @@ import { AlertAction, State, Context, AlertType } from '../types'; import { Logger } from '../../../../../src/core/server'; import { transformActionParams } from './transform_action_params'; import { PluginStartContract as ActionsPluginStartContract } from '../../../../plugins/actions/server'; -import { IEventLogger, IEvent } from '../../../event_log/server'; +import { IEventLogger, IEvent, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; interface CreateExecutionHandlerOptions { @@ -96,7 +96,7 @@ export function createExecutionHandler({ instance_id: alertInstanceId, }, saved_objects: [ - { type: 'alert', id: alertId, ...namespace }, + { rel: SAVED_OBJECT_REL_PRIMARY, type: 'alert', id: alertId, ...namespace }, { type: 'action', id: action.id, ...namespace }, ], }, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 26d8a1d1777c0..35a0018049c33 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -172,6 +172,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -234,6 +235,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -254,6 +256,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -274,6 +277,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, Object { @@ -351,6 +355,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -371,6 +376,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -568,6 +574,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 26970dc6b2b0d..bf005301adc07 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -25,7 +25,7 @@ import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/ import { taskInstanceToAlertTaskInstance } from './alert_task_instance'; import { AlertInstances } from '../alert_instance/alert_instance'; import { EVENT_LOG_ACTIONS } from '../plugin'; -import { IEvent, IEventLogger } from '../../../event_log/server'; +import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error'; const FALLBACK_RETRY_INTERVAL: IntervalSchedule = { interval: '5m' }; @@ -174,7 +174,16 @@ export class TaskRunner { const alertLabel = `${this.alertType.id}:${alertId}: '${name}'`; const event: IEvent = { event: { action: EVENT_LOG_ACTIONS.execute }, - kibana: { saved_objects: [{ type: 'alert', id: alertId, namespace }] }, + kibana: { + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'alert', + id: alertId, + namespace, + }, + ], + }, }; eventLogger.startTiming(event); @@ -393,7 +402,14 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst alerting: { instance_id: id, }, - saved_objects: [{ type: 'alert', id: params.alertId, namespace: params.namespace }], + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'alert', + id: params.alertId, + namespace: params.namespace, + }, + ], }, message, }; diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index f487e9262e50e..0a858969c4f6a 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -86,6 +86,10 @@ }, "saved_objects": { "properties": { + "rel": { + "type": "keyword", + "ignore_above": 1024 + }, "namespace": { "type": "keyword", "ignore_above": 1024 diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 9c923fe77d035..57fe90a8e876e 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -65,6 +65,7 @@ export const EventSchema = schema.maybe( saved_objects: schema.maybe( schema.arrayOf( schema.object({ + rel: ecsString(), namespace: ecsString(), id: ecsString(), type: ecsString(), diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index 8cc2c74b60e57..fd149d132031e 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -24,6 +24,11 @@ exports.EcsKibanaExtensionsMappings = { saved_objects: { type: 'nested', properties: { + // relation; currently only supports "primary" or not set + rel: { + type: 'keyword', + ignore_above: 1024, + }, // relevant kibana space namespace: { type: 'keyword', @@ -58,6 +63,7 @@ exports.EcsEventLogProperties = [ 'user.name', 'kibana.server_uuid', 'kibana.alerting.instance_id', + 'kibana.saved_objects.rel', 'kibana.saved_objects.namespace', 'kibana.saved_objects.id', 'kibana.saved_objects.name', 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 f79962a324131..66c16d0ddf383 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 @@ -236,6 +236,13 @@ describe('queryEventsBySavedObject', () => { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, { term: { 'kibana.saved_objects.type': { @@ -319,6 +326,13 @@ describe('queryEventsBySavedObject', () => { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, { term: { 'kibana.saved_objects.type': { @@ -388,6 +402,13 @@ describe('queryEventsBySavedObject', () => { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, { term: { 'kibana.saved_objects.type': { 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 47d273b9981e3..c0ff87234c09d 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 @@ -7,7 +7,7 @@ import { reject, isUndefined } from 'lodash'; import { SearchResponse, Client } from 'elasticsearch'; import { Logger, ClusterClient } from '../../../../../src/core/server'; -import { IEvent } from '../types'; +import { IEvent, SAVED_OBJECT_REL_PRIMARY } from '../types'; import { FindOptionsType } from '../event_log_client'; export type EsClusterClient = Pick; @@ -155,6 +155,13 @@ export class ClusterClientAdapter { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: SAVED_OBJECT_REL_PRIMARY, + }, + }, + }, { term: { 'kibana.saved_objects.type': { diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts index 6a745931420c0..2bda194a65d13 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -150,6 +150,35 @@ describe('EventLogger', () => { message = await waitForLogMessage(systemLogger); expect(message).toMatch(/invalid event logged.*action.*undefined.*/); }); + + test('logs warnings when writing invalid events', async () => { + service.registerProviderActions('provider', ['action-a']); + eventLogger = service.getLogger({}); + + eventLogger.logEvent(({ event: { PROVIDER: 'provider' } } as unknown) as IEvent); + let message = await waitForLogMessage(systemLogger); + expect(message).toMatch(/invalid event logged.*provider.*undefined.*/); + + const event: IEvent = { + event: { + provider: 'provider', + action: 'action-a', + }, + kibana: { + saved_objects: [ + { + rel: 'ZZZ-primary', + namespace: 'default', + type: 'event_log_test', + id: '123', + }, + ], + }, + }; + eventLogger.logEvent(event); + message = await waitForLogMessage(systemLogger); + expect(message).toMatch(/invalid rel property.*ZZZ-primary.*/); + }); }); // return the next logged event; throw if not an event diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index bcfd7bd45a6f5..1a710a6fa4865 100644 --- a/x-pack/plugins/event_log/server/event_logger.ts +++ b/x-pack/plugins/event_log/server/event_logger.ts @@ -19,6 +19,7 @@ import { ECS_VERSION, EventSchema, } from './types'; +import { SAVED_OBJECT_REL_PRIMARY } from './types'; type SystemLogger = Plugin['systemLogger']; @@ -118,6 +119,8 @@ const RequiredEventSchema = schema.object({ action: schema.string({ minLength: 1 }), }); +const ValidSavedObjectRels = new Set([undefined, SAVED_OBJECT_REL_PRIMARY]); + function validateEvent(eventLogService: IEventLogService, event: IEvent): IValidatedEvent { if (event?.event == null) { throw new Error(`no "event" property`); @@ -137,7 +140,17 @@ function validateEvent(eventLogService: IEventLogService, event: IEvent): IValid } // could throw an error - return EventSchema.validate(event); + const result = EventSchema.validate(event); + + if (result?.kibana?.saved_objects?.length) { + for (const so of result?.kibana?.saved_objects) { + if (!ValidSavedObjectRels.has(so.rel)) { + throw new Error(`invalid rel property in saved_objects: "${so.rel}"`); + } + } + } + + return result; } export const EVENT_LOGGED_PREFIX = `event logged: `; diff --git a/x-pack/plugins/event_log/server/index.ts b/x-pack/plugins/event_log/server/index.ts index b7fa25cb6eb9c..0612b5319c15b 100644 --- a/x-pack/plugins/event_log/server/index.ts +++ b/x-pack/plugins/event_log/server/index.ts @@ -8,6 +8,12 @@ import { PluginInitializerContext } from 'src/core/server'; import { ConfigSchema } from './types'; import { Plugin } from './plugin'; -export { IEventLogService, IEventLogger, IEventLogClientService, IEvent } from './types'; +export { + IEventLogService, + IEventLogger, + IEventLogClientService, + IEvent, + SAVED_OBJECT_REL_PRIMARY, +} from './types'; export const config = { schema: ConfigSchema }; export const plugin = (context: PluginInitializerContext) => new Plugin(context); diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index baf53ef447914..58be6707b0373 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -13,6 +13,8 @@ import { IEvent } from '../generated/schemas'; import { FindOptionsType } from './event_log_client'; import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; +export const SAVED_OBJECT_REL_PRIMARY = 'primary'; + export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), logEntries: schema.boolean({ defaultValue: false }), diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts index c5f3e65581df9..9622715e87e55 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts @@ -40,7 +40,7 @@ export const logEventRoute = (router: IRouter, eventLogger: IEventLogger, logger } catch (ex) { logger.info(`log event error: ${ex}`); await context.core.savedObjects.client.create('event_log_test', {}, { id }); - logger.info(`created saved object`); + logger.info(`created saved object ${id}`); } eventLogger.logEvent(event); logger.info(`logged`); diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index d7bbc29bd861e..f3a3d58336b1d 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -205,6 +205,7 @@ export default function({ getService }: FtrProviderContext) { kibana: { saved_objects: [ { + rel: 'primary', namespace: 'default', type: 'event_log_test', id, diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts index 31668e8345275..361d80aaedd41 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts @@ -101,7 +101,7 @@ export default function({ getService }: FtrProviderContext) { const eventId = uuid.v4(); const event: IEvent = { event: { action: 'action1', provider: 'provider4' }, - kibana: { saved_objects: [{ type: 'event_log_test', id: eventId }] }, + kibana: { saved_objects: [{ rel: 'primary', type: 'event_log_test', id: eventId }] }, }; await logTestEvent(eventId, event); From aa8cb620bb63afbb631e81b7e5a507e8075a1b74 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 30 Apr 2020 08:05:16 +0200 Subject: [PATCH 022/122] Allow to define and update a defaultPath for applications (#64498) * add defaultPath to `AppBase` and use it in `navigateToApp` * add removeSlashes util * adapt `toNavLink` to handle defaultPath * update generated doc * codestyle * add FTR test * address comments * add tests --- ...-plugin-core-public.appbase.defaultpath.md | 13 +++ .../kibana-plugin-core-public.appbase.md | 1 + ...a-plugin-core-public.appupdatablefields.md | 2 +- ...kibana-plugin-core-public.chromenavlink.md | 2 +- ...na-plugin-core-public.chromenavlink.url.md | 6 +- .../application/application_service.test.ts | 87 ++++++++++++++++++- .../application/application_service.tsx | 12 ++- src/core/public/application/types.ts | 15 +++- src/core/public/application/utils.test.ts | 71 +++++++++++++++ src/core/public/application/utils.ts | 54 ++++++++++++ src/core/public/chrome/nav_links/nav_link.ts | 18 ++-- .../chrome/nav_links/to_nav_link.test.ts | 32 +++++++ .../public/chrome/nav_links/to_nav_link.ts | 11 ++- src/core/public/chrome/ui/header/nav_link.tsx | 2 +- src/core/public/public.api.md | 4 +- .../core_plugins/application_status.ts | 26 ++++++ 16 files changed, 320 insertions(+), 36 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md create mode 100644 src/core/public/application/utils.test.ts create mode 100644 src/core/public/application/utils.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md b/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md new file mode 100644 index 0000000000000..51492756ef232 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appbase.defaultpath.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppBase](./kibana-plugin-core-public.appbase.md) > [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md) + +## AppBase.defaultPath property + +Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the `path` option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. + +Signature: + +```typescript +defaultPath?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appbase.md b/docs/development/core/public/kibana-plugin-core-public.appbase.md index b73785647f23c..7b624f12ac1df 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appbase.md +++ b/docs/development/core/public/kibana-plugin-core-public.appbase.md @@ -18,6 +18,7 @@ export interface AppBase | [capabilities](./kibana-plugin-core-public.appbase.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | | [category](./kibana-plugin-core-public.appbase.category.md) | AppCategory | The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference | | [chromeless](./kibana-plugin-core-public.appbase.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | +| [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md) | string | Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the path option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)\`, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. | | [euiIconType](./kibana-plugin-core-public.appbase.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | | [icon](./kibana-plugin-core-public.appbase.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | | [id](./kibana-plugin-core-public.appbase.id.md) | string | The unique identifier of the application | diff --git a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md index cdf9171a46aed..3d8b5d115c8a2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md +++ b/docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md @@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug Signature: ```typescript -export declare type AppUpdatableFields = Pick; +export declare type AppUpdatableFields = Pick; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md index 1cc1a1194a537..a9fabb38df869 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.md @@ -29,5 +29,5 @@ export interface ChromeNavLink | [subUrlBase](./kibana-plugin-core-public.chromenavlink.suburlbase.md) | string | A url base that legacy apps can set to match deep URLs to an application. | | [title](./kibana-plugin-core-public.chromenavlink.title.md) | string | The title of the application. | | [tooltip](./kibana-plugin-core-public.chromenavlink.tooltip.md) | string | A tooltip shown when hovering over an app link. | -| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string | A url that legacy apps can set to deep link into their applications. | +| [url](./kibana-plugin-core-public.chromenavlink.url.md) | string | The route used to open the [default path](./kibana-plugin-core-public.appbase.defaultpath.md) of an application. If unset, baseUrl will be used instead. | diff --git a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md index 0c415ed1a7fad..1e0b890015993 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md @@ -4,11 +4,7 @@ ## ChromeNavLink.url property -> Warning: This API is now obsolete. -> -> - -A url that legacy apps can set to deep link into their applications. +The route used to open the [default path](./kibana-plugin-core-public.appbase.defaultpath.md) of an application. If unset, `baseUrl` will be used instead. Signature: diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index c25918c6b7328..e29837aecb125 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -87,7 +87,7 @@ describe('#setup()', () => { ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`); }); - it('allows to register a statusUpdater for the application', async () => { + it('allows to register an AppUpdater for the application', async () => { const setup = service.setup(setupDeps); const pluginId = Symbol('plugin'); @@ -118,6 +118,7 @@ describe('#setup()', () => { updater$.next(app => ({ status: AppStatus.inaccessible, tooltip: 'App inaccessible due to reason', + defaultPath: 'foo/bar', })); applications = await applications$.pipe(take(1)).toPromise(); @@ -128,6 +129,7 @@ describe('#setup()', () => { legacy: false, navLinkStatus: AppNavLinkStatus.default, status: AppStatus.inaccessible, + defaultPath: 'foo/bar', tooltip: 'App inaccessible due to reason', }) ); @@ -209,7 +211,7 @@ describe('#setup()', () => { }); }); - describe('registerAppStatusUpdater', () => { + describe('registerAppUpdater', () => { it('updates status fields', async () => { const setup = service.setup(setupDeps); @@ -413,6 +415,36 @@ describe('#setup()', () => { }) ); }); + + it('allows to update the basePath', async () => { + const setup = service.setup(setupDeps); + + const pluginId = Symbol('plugin'); + setup.register(pluginId, createApp({ id: 'app1' })); + + const updater = new BehaviorSubject(app => ({})); + setup.registerAppUpdater(updater); + + const start = await service.start(startDeps); + await start.navigateToApp('app1'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined); + MockHistory.push.mockClear(); + + updater.next(app => ({ defaultPath: 'default-path' })); + await start.navigateToApp('app1'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/default-path', undefined); + MockHistory.push.mockClear(); + + updater.next(app => ({ defaultPath: 'another-path' })); + await start.navigateToApp('app1'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/another-path', undefined); + MockHistory.push.mockClear(); + + updater.next(app => ({})); + await start.navigateToApp('app1'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined); + MockHistory.push.mockClear(); + }); }); it("`registerMountContext` calls context container's registerContext", () => { @@ -676,6 +708,57 @@ describe('#start()', () => { expect(MockHistory.push).toHaveBeenCalledWith('/custom/path#/hash/router/path', undefined); }); + it('preserves trailing slash when path contains a hash', async () => { + const { register } = service.setup(setupDeps); + + register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/app-path' })); + + const { navigateToApp } = await service.start(startDeps); + await navigateToApp('app2', { path: '#/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path#/', undefined); + MockHistory.push.mockClear(); + + await navigateToApp('app2', { path: '#/foo/bar/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path#/foo/bar/', undefined); + MockHistory.push.mockClear(); + + await navigateToApp('app2', { path: '/path#/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path#/', undefined); + MockHistory.push.mockClear(); + + await navigateToApp('app2', { path: '/path#/hash/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path#/hash/', undefined); + MockHistory.push.mockClear(); + + await navigateToApp('app2', { path: '/path/' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path', undefined); + MockHistory.push.mockClear(); + }); + + it('appends the defaultPath when the path parameter is not specified', async () => { + const { register } = service.setup(setupDeps); + + register(Symbol(), createApp({ id: 'app1', defaultPath: 'default/path' })); + register( + Symbol(), + createApp({ id: 'app2', appRoute: '/custom-app-path', defaultPath: '/my-base' }) + ); + + const { navigateToApp } = await service.start(startDeps); + + await navigateToApp('app1', { path: 'defined-path' }); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/defined-path', undefined); + + await navigateToApp('app1', {}); + expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/default/path', undefined); + + await navigateToApp('app2', { path: 'defined-path' }); + expect(MockHistory.push).toHaveBeenCalledWith('/custom-app-path/defined-path', undefined); + + await navigateToApp('app2', {}); + expect(MockHistory.push).toHaveBeenCalledWith('/custom-app-path/my-base', undefined); + }); + it('includes state if specified', async () => { const { register } = service.setup(setupDeps); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 1c9492d81c7f6..bafa1932e5e92 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -46,6 +46,7 @@ import { Mounter, } from './types'; import { getLeaveAction, isConfirmAction } from './application_leave'; +import { appendAppPath } from './utils'; interface SetupDeps { context: ContextSetup; @@ -81,13 +82,7 @@ const getAppUrl = (mounters: Map, appId: string, path: string = const appBasePath = mounters.get(appId)?.appRoute ? `/${mounters.get(appId)!.appRoute}` : `/app/${appId}`; - - // Only preppend slash if not a hash or query path - path = path.startsWith('#') || path.startsWith('?') ? path : `/${path}`; - - return `${appBasePath}${path}` - .replace(/\/{2,}/g, '/') // Remove duplicate slashes - .replace(/\/$/, ''); // Remove trailing slash + return appendAppPath(appBasePath, path); }; const allApplicationsFilter = '__ALL__'; @@ -290,6 +285,9 @@ export class ApplicationService { }, navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => { if (await this.shouldNavigate(overlays)) { + if (path === undefined) { + path = applications$.value.get(appId)?.defaultPath; + } this.appLeaveHandlers.delete(this.currentAppId$.value!); this.navigate!(getAppUrl(availableMounters, appId, path), state); this.currentAppId$.next(appId); diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 318afb652999e..0734e178033e2 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -66,6 +66,13 @@ export interface AppBase { */ navLinkStatus?: AppNavLinkStatus; + /** + * Allow to define the default path a user should be directed to when navigating to the app. + * When defined, this value will be used as a default for the `path` option when calling {@link ApplicationStart.navigateToApp | navigateToApp}`, + * and will also be appended to the {@link ChromeNavLink | application navLink} in the navigation bar. + */ + defaultPath?: string; + /** * An {@link AppUpdater} observable that can be used to update the application {@link AppUpdatableFields} at runtime. * @@ -187,7 +194,10 @@ export enum AppNavLinkStatus { * Defines the list of fields that can be updated via an {@link AppUpdater}. * @public */ -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick< + AppBase, + 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' +>; /** * Updater for applications. @@ -642,7 +652,8 @@ export interface ApplicationStart { * Navigate to a given app * * @param appId - * @param options.path - optional path inside application to deep link to + * @param options.path - optional path inside application to deep link to. + * If undefined, will use {@link AppBase.defaultPath | the app's default path}` as default. * @param options.state - optional state to forward to the application */ navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise; diff --git a/src/core/public/application/utils.test.ts b/src/core/public/application/utils.test.ts new file mode 100644 index 0000000000000..7ed0919f88c61 --- /dev/null +++ b/src/core/public/application/utils.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { removeSlashes, appendAppPath } from './utils'; + +describe('removeSlashes', () => { + it('only removes duplicates by default', () => { + expect(removeSlashes('/some//url//to//')).toEqual('/some/url/to/'); + expect(removeSlashes('some/////other//url')).toEqual('some/other/url'); + }); + + it('remove trailing slash when `trailing` is true', () => { + expect(removeSlashes('/some//url//to//', { trailing: true })).toEqual('/some/url/to'); + }); + + it('remove leading slash when `leading` is true', () => { + expect(removeSlashes('/some//url//to//', { leading: true })).toEqual('some/url/to/'); + }); + + it('does not removes duplicates when `duplicates` is false', () => { + expect(removeSlashes('/some//url//to/', { leading: true, duplicates: false })).toEqual( + 'some//url//to/' + ); + expect(removeSlashes('/some//url//to/', { trailing: true, duplicates: false })).toEqual( + '/some//url//to' + ); + }); + + it('accept mixed options', () => { + expect( + removeSlashes('/some//url//to/', { leading: true, duplicates: false, trailing: true }) + ).toEqual('some//url//to'); + expect( + removeSlashes('/some//url//to/', { leading: true, duplicates: true, trailing: true }) + ).toEqual('some/url/to'); + }); +}); + +describe('appendAppPath', () => { + it('appends the appBasePath with given path', () => { + expect(appendAppPath('/app/my-app', '/some-path')).toEqual('/app/my-app/some-path'); + expect(appendAppPath('/app/my-app/', 'some-path')).toEqual('/app/my-app/some-path'); + expect(appendAppPath('/app/my-app', 'some-path')).toEqual('/app/my-app/some-path'); + expect(appendAppPath('/app/my-app', '')).toEqual('/app/my-app'); + }); + + it('preserves the trailing slash only if included in the hash', () => { + expect(appendAppPath('/app/my-app', '/some-path/')).toEqual('/app/my-app/some-path'); + expect(appendAppPath('/app/my-app', '/some-path#/')).toEqual('/app/my-app/some-path#/'); + expect(appendAppPath('/app/my-app', '/some-path#/hash/')).toEqual( + '/app/my-app/some-path#/hash/' + ); + expect(appendAppPath('/app/my-app', '/some-path#/hash')).toEqual('/app/my-app/some-path#/hash'); + }); +}); diff --git a/src/core/public/application/utils.ts b/src/core/public/application/utils.ts new file mode 100644 index 0000000000000..048f195fe1223 --- /dev/null +++ b/src/core/public/application/utils.ts @@ -0,0 +1,54 @@ +/* + * 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. + */ + +/** + * Utility to remove trailing, leading or duplicate slashes. + * By default will only remove duplicates. + */ +export const removeSlashes = ( + url: string, + { + trailing = false, + leading = false, + duplicates = true, + }: { trailing?: boolean; leading?: boolean; duplicates?: boolean } = {} +): string => { + if (duplicates) { + url = url.replace(/\/{2,}/g, '/'); + } + if (trailing) { + url = url.replace(/\/$/, ''); + } + if (leading) { + url = url.replace(/^\//, ''); + } + return url; +}; + +export const appendAppPath = (appBasePath: string, path: string = '') => { + // Only prepend slash if not a hash or query path + path = path === '' || path.startsWith('#') || path.startsWith('?') ? path : `/${path}`; + // Do not remove trailing slash when in hashbang + const removeTrailing = path.indexOf('#') === -1; + return removeSlashes(`${appBasePath}${path}`, { + trailing: removeTrailing, + duplicates: true, + leading: false, + }); +}; diff --git a/src/core/public/chrome/nav_links/nav_link.ts b/src/core/public/chrome/nav_links/nav_link.ts index d0ef2aeb265fe..fb2972735c2b7 100644 --- a/src/core/public/chrome/nav_links/nav_link.ts +++ b/src/core/public/chrome/nav_links/nav_link.ts @@ -44,6 +44,12 @@ export interface ChromeNavLink { */ readonly baseUrl: string; + /** + * The route used to open the {@link AppBase.defaultPath | default path } of an application. + * If unset, `baseUrl` will be used instead. + */ + readonly url?: string; + /** * An ordinal used to sort nav links relative to one another for display. */ @@ -99,18 +105,6 @@ export interface ChromeNavLink { */ readonly linkToLastSubUrl?: boolean; - /** - * A url that legacy apps can set to deep link into their applications. - * - * @internalRemarks - * Currently used by the "lastSubUrl" feature legacy/ui/chrome. This should - * be removed once the ApplicationService is implemented and mounting apps. At that - * time, each app can handle opening to the previous location when they are mounted. - * - * @deprecated - */ - readonly url?: string; - /** * Indicates whether or not this app is currently on the screen. * diff --git a/src/core/public/chrome/nav_links/to_nav_link.test.ts b/src/core/public/chrome/nav_links/to_nav_link.test.ts index 23fdabe0f3430..4c319873af804 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.test.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.test.ts @@ -85,6 +85,38 @@ describe('toNavLink', () => { expect(link.properties.baseUrl).toEqual('http://localhost/base-path/my-route/my-path'); }); + it('generates the `url` property', () => { + let link = toNavLink( + app({ + appRoute: '/my-route/my-path', + }), + basePath + ); + expect(link.properties.url).toEqual('http://localhost/base-path/my-route/my-path'); + + link = toNavLink( + app({ + appRoute: '/my-route/my-path', + defaultPath: 'some/default/path', + }), + basePath + ); + expect(link.properties.url).toEqual( + 'http://localhost/base-path/my-route/my-path/some/default/path' + ); + }); + + it('does not generate `url` for legacy app', () => { + const link = toNavLink( + legacyApp({ + appUrl: '/my-legacy-app/#foo', + defaultPath: '/some/default/path', + }), + basePath + ); + expect(link.properties.url).toBeUndefined(); + }); + it('uses appUrl when converting legacy applications', () => { expect( toNavLink( diff --git a/src/core/public/chrome/nav_links/to_nav_link.ts b/src/core/public/chrome/nav_links/to_nav_link.ts index 18e4b7b26b6ba..f79b1df77f8e1 100644 --- a/src/core/public/chrome/nav_links/to_nav_link.ts +++ b/src/core/public/chrome/nav_links/to_nav_link.ts @@ -20,9 +20,11 @@ import { App, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application'; import { IBasePath } from '../../http'; import { NavLinkWrapper } from './nav_link'; +import { appendAppPath } from '../../application/utils'; export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWrapper { const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default; + const baseUrl = isLegacyApp(app) ? basePath.prepend(app.appUrl) : basePath.prepend(app.appRoute!); return new NavLinkWrapper({ ...app, hidden: useAppStatus @@ -30,9 +32,12 @@ export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWra : app.navLinkStatus === AppNavLinkStatus.hidden, disabled: useAppStatus ? false : app.navLinkStatus === AppNavLinkStatus.disabled, legacy: isLegacyApp(app), - baseUrl: isLegacyApp(app) - ? relativeToAbsolute(basePath.prepend(app.appUrl)) - : relativeToAbsolute(basePath.prepend(app.appRoute!)), + baseUrl: relativeToAbsolute(baseUrl), + ...(isLegacyApp(app) + ? {} + : { + url: relativeToAbsolute(appendAppPath(baseUrl, app.defaultPath)), + }), }); } diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index 52b59c53b658c..d97ef477c2ee0 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -53,7 +53,7 @@ export function euiNavLink( order, tooltip, } = navLink; - let href = navLink.baseUrl; + let href = navLink.url ?? navLink.baseUrl; if (legacy) { href = url && !active ? url : baseUrl; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index b92bb209d2607..af06b207889c2 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -36,6 +36,7 @@ export interface AppBase { capabilities?: Partial; category?: AppCategory; chromeless?: boolean; + defaultPath?: string; euiIconType?: string; icon?: string; id: string; @@ -168,7 +169,7 @@ export enum AppStatus { export type AppUnmount = () => void; // @public -export type AppUpdatableFields = Pick; +export type AppUpdatableFields = Pick; // @public export type AppUpdater = (app: AppBase) => Partial | undefined; @@ -290,7 +291,6 @@ export interface ChromeNavLink { readonly subUrlBase?: string; readonly title: string; readonly tooltip?: string; - // @deprecated readonly url?: string; } diff --git a/test/plugin_functional/test_suites/core_plugins/application_status.ts b/test/plugin_functional/test_suites/core_plugins/application_status.ts index b6d13a5604011..c384e41851e15 100644 --- a/test/plugin_functional/test_suites/core_plugins/application_status.ts +++ b/test/plugin_functional/test_suites/core_plugins/application_status.ts @@ -17,6 +17,7 @@ * under the License. */ +import url from 'url'; import expect from '@kbn/expect'; import { AppNavLinkStatus, @@ -26,6 +27,15 @@ import { import { PluginFunctionalProviderContext } from '../../services'; import '../../plugins/core_app_status/public/types'; +const getKibanaUrl = (pathname?: string, search?: string) => + url.format({ + protocol: 'http:', + hostname: process.env.TEST_KIBANA_HOST || 'localhost', + port: process.env.TEST_KIBANA_PORT || '5620', + pathname, + search, + }); + // eslint-disable-next-line import/no-default-export export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { const PageObjects = getPageObjects(['common']); @@ -97,6 +107,22 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(await testSubjects.exists('appStatusApp')).to.eql(true); }); + it('allows to change the defaultPath of an application', async () => { + let link = await appsMenu.getLink('App Status'); + expect(link!.href).to.eql(getKibanaUrl('/app/app_status')); + + await setAppStatus({ + defaultPath: '/arbitrary/path', + }); + + link = await appsMenu.getLink('App Status'); + expect(link!.href).to.eql(getKibanaUrl('/app/app_status/arbitrary/path')); + + await navigateToApp('app_status'); + expect(await testSubjects.exists('appStatusApp')).to.eql(true); + expect(await browser.getCurrentUrl()).to.eql(getKibanaUrl('/app/app_status/arbitrary/path')); + }); + it('can change the state of the currently mounted app', async () => { await setAppStatus({ status: AppStatus.accessible, From 6e2691358fdb59af717a9aaa2b32887b9cdf2d2a Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 30 Apr 2020 10:28:14 +0200 Subject: [PATCH 023/122] add generic typings for SavedObjectMigrationFn (#63943) * add generic typings for SavedObjectMigrationFn * change default attributes type to unknown * update generated doc * adapt new calls * update generated doc * update migration example * fix merge conflicts --- ...ugin-core-server.savedobjectmigrationfn.md | 32 ++++++++--- ...gin-core-server.savedobjectsanitizeddoc.md | 2 +- ...n-core-server.savedobjectunsanitizeddoc.md | 2 +- src/core/MIGRATION_EXAMPLES.md | 2 +- .../saved_objects/migrations/core/index.ts | 2 +- .../server/saved_objects/migrations/mocks.ts | 43 +++++++++++++++ .../server/saved_objects/migrations/types.ts | 34 ++++++++---- .../saved_objects_service.mock.ts | 2 + .../saved_objects/serialization/types.ts | 8 +-- src/core/server/server.api.md | 6 +- .../dashboard_migrations.test.ts | 31 ++++++----- .../saved_objects/dashboard_migrations.ts | 10 ++-- .../saved_objects/migrate_match_all_query.ts | 2 +- .../saved_objects/migrations_730.test.ts | 14 ++--- .../saved_objects/index_pattern_migrations.ts | 4 +- .../server/saved_objects/search_migrations.ts | 14 ++--- .../server/saved_objects/tsvb_telemetry.ts | 2 +- .../saved_objects/visualization_migrations.ts | 55 ++++++++++--------- .../graph/server/saved_objects/migrations.ts | 2 +- x-pack/plugins/lens/server/migrations.ts | 9 ++- .../saved_objects/migrations/migrate_6x.ts | 2 +- 21 files changed, 177 insertions(+), 101 deletions(-) create mode 100644 src/core/server/saved_objects/migrations/mocks.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md index a502c40db0cd8..a3294fb0a087a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectmigrationfn.md @@ -9,22 +9,36 @@ A migration function for a [saved object type](./kibana-plugin-core-server.saved Signature: ```typescript -export declare type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; +export declare type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; ``` ## Example ```typescript -const migrateProperty: SavedObjectMigrationFn = (doc, { log }) => { - if(doc.attributes.someProp === null) { - log.warn('Skipping migration'); - } else { - doc.attributes.someProp = migrateProperty(doc.attributes.someProp); - } - - return doc; +interface TypeV1Attributes { + someKey: string; + obsoleteProperty: number; } +interface TypeV2Attributes { + someKey: string; + newProperty: string; +} + +const migrateToV2: SavedObjectMigrationFn = (doc, { log }) => { + const { obsoleteProperty, ...otherAttributes } = doc.attributes; + // instead of mutating `doc` we make a shallow copy so that we can use separate types for the input + // and output attributes. We don't need to make a deep copy, we just need to ensure that obsolete + // attributes are not present on the returned doc. + return { + ...doc, + attributes: { + ...otherAttributes, + newProperty: migrate(obsoleteProperty), + }, + }; +}; + ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md index 6d4e252fe7532..3f4090619edbf 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md @@ -9,5 +9,5 @@ Describes Saved Object documents that have passed through the migration framewor Signature: ```typescript -export declare type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; +export declare type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md index be51400addbbc..8e2395ee6310d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md @@ -9,5 +9,5 @@ Describes Saved Object documents from Kibana < 7.0.0 which don't have a `refe Signature: ```typescript -export declare type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +export declare type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; ``` diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 8c5fe4875aaea..c91c00bc1aa02 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -957,7 +957,7 @@ const migration = (doc, log) => {...} Would be converted to: ```typescript -const migration: SavedObjectMigrationFn = (doc, { log }) => {...} +const migration: SavedObjectMigrationFn = (doc, { log }) => {...} ``` ### Remarks diff --git a/src/core/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts index 466d399f653cd..f7274740ea5fe 100644 --- a/src/core/server/saved_objects/migrations/core/index.ts +++ b/src/core/server/saved_objects/migrations/core/index.ts @@ -21,5 +21,5 @@ export { DocumentMigrator } from './document_migrator'; export { IndexMigrator } from './index_migrator'; export { buildActiveMappings } from './build_active_mappings'; export { CallCluster } from './call_cluster'; -export { LogFn } from './migration_logger'; +export { LogFn, SavedObjectsMigrationLogger } from './migration_logger'; export { MigrationResult, MigrationStatus } from './migration_coordinator'; diff --git a/src/core/server/saved_objects/migrations/mocks.ts b/src/core/server/saved_objects/migrations/mocks.ts new file mode 100644 index 0000000000000..76a890d26bfa0 --- /dev/null +++ b/src/core/server/saved_objects/migrations/mocks.ts @@ -0,0 +1,43 @@ +/* + * 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 { SavedObjectMigrationContext } from './types'; +import { SavedObjectsMigrationLogger } from './core'; + +const createLoggerMock = (): jest.Mocked => { + const mock = { + debug: jest.fn(), + info: jest.fn(), + warning: jest.fn(), + warn: jest.fn(), + }; + + return mock; +}; + +const createContextMock = (): jest.Mocked => { + const mock = { + log: createLoggerMock(), + }; + return mock; +}; + +export const migrationMocks = { + createContext: createContextMock, +}; diff --git a/src/core/server/saved_objects/migrations/types.ts b/src/core/server/saved_objects/migrations/types.ts index 6bc085dde872e..85f15b4c18b66 100644 --- a/src/core/server/saved_objects/migrations/types.ts +++ b/src/core/server/saved_objects/migrations/types.ts @@ -26,23 +26,37 @@ import { SavedObjectsMigrationLogger } from './core/migration_logger'; * * @example * ```typescript - * const migrateProperty: SavedObjectMigrationFn = (doc, { log }) => { - * if(doc.attributes.someProp === null) { - * log.warn('Skipping migration'); - * } else { - * doc.attributes.someProp = migrateProperty(doc.attributes.someProp); - * } + * interface TypeV1Attributes { + * someKey: string; + * obsoleteProperty: number; + * } * - * return doc; + * interface TypeV2Attributes { + * someKey: string; + * newProperty: string; * } + * + * const migrateToV2: SavedObjectMigrationFn = (doc, { log }) => { + * const { obsoleteProperty, ...otherAttributes } = doc.attributes; + * // instead of mutating `doc` we make a shallow copy so that we can use separate types for the input + * // and output attributes. We don't need to make a deep copy, we just need to ensure that obsolete + * // attributes are not present on the returned doc. + * return { + * ...doc, + * attributes: { + * ...otherAttributes, + * newProperty: migrate(obsoleteProperty), + * }, + * }; + * }; * ``` * * @public */ -export type SavedObjectMigrationFn = ( - doc: SavedObjectUnsanitizedDoc, +export type SavedObjectMigrationFn = ( + doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext -) => SavedObjectUnsanitizedDoc; +) => SavedObjectUnsanitizedDoc; /** * Migration context provided when invoking a {@link SavedObjectMigrationFn | migration handler} diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index 7ba4613c857d7..4e1f5981d6a41 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -31,6 +31,7 @@ import { savedObjectsClientProviderMock } from './service/lib/scoped_client_prov import { savedObjectsRepositoryMock } from './service/lib/repository.mock'; import { savedObjectsClientMock } from './service/saved_objects_client.mock'; import { typeRegistryMock } from './saved_objects_type_registry.mock'; +import { migrationMocks } from './migrations/mocks'; import { ServiceStatusLevels } from '../status'; type SavedObjectsServiceContract = PublicMethodsOf; @@ -105,4 +106,5 @@ export const savedObjectsServiceMock = { createSetupContract: createSetupContractMock, createInternalStartContract: createInternalStartContractMock, createStartContract: createStartContractMock, + createMigrationContext: migrationMocks.createContext, }; diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index a33e16895078e..acd2c7b5284aa 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -47,8 +47,8 @@ export interface SavedObjectsRawDocSource { /** * Saved Object base document */ -interface SavedObjectDoc { - attributes: any; +interface SavedObjectDoc { + attributes: T; id?: string; // NOTE: SavedObjectDoc is used for uncreated objects where `id` is optional type: string; namespace?: string; @@ -69,7 +69,7 @@ interface Referencable { * * @public */ -export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; /** * Describes Saved Object documents that have passed through the migration @@ -77,4 +77,4 @@ export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; * * @public */ -export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; +export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index dc1c9d379d508..e8b77a8570291 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1680,7 +1680,7 @@ export interface SavedObjectMigrationContext { } // @public -export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; +export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; // @public export interface SavedObjectMigrationMap { @@ -1708,7 +1708,7 @@ export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOpti // Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts // // @public -export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; +export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; // @public (undocumented) export interface SavedObjectsBaseOptions { @@ -2311,7 +2311,7 @@ export class SavedObjectTypeRegistry { } // @public -export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; // @public export type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest; diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts index 9829498118cc0..22ed18f75c652 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -18,14 +18,17 @@ */ import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { savedObjectsServiceMock } from '../../../../core/server/mocks'; import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +const contextMock = savedObjectsServiceMock.createMigrationContext(); + describe('dashboard', () => { describe('7.0.0', () => { const migration = migrations['7.0.0']; test('skips error on empty object', () => { - expect(migration({} as SavedObjectUnsanitizedDoc)).toMatchInlineSnapshot(` + expect(migration({} as SavedObjectUnsanitizedDoc, contextMock)).toMatchInlineSnapshot(` Object { "references": Array [], } @@ -44,7 +47,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -83,7 +86,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -122,7 +125,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "kibanaSavedObjectMeta": Object { @@ -160,7 +163,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "kibanaSavedObjectMeta": Object { @@ -198,7 +201,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -237,7 +240,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -291,7 +294,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { @@ -331,7 +334,7 @@ Object { panelsJSON: 123, }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": 123, @@ -349,7 +352,7 @@ Object { panelsJSON: '{123abc}', }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": "{123abc}", @@ -367,7 +370,7 @@ Object { panelsJSON: '{}', }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": "{}", @@ -385,7 +388,7 @@ Object { panelsJSON: '[{"id":"123"}]', }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": "[{\\"id\\":\\"123\\"}]", @@ -403,7 +406,7 @@ Object { panelsJSON: '[{"type":"visualization"}]', }, } as SavedObjectUnsanitizedDoc; - expect(migration(doc)).toMatchInlineSnapshot(` + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` Object { "attributes": Object { "panelsJSON": "[{\\"type\\":\\"visualization\\"}]", @@ -422,7 +425,7 @@ Object { '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, } as SavedObjectUnsanitizedDoc; - const migratedDoc = migration(doc); + const migratedDoc = migration(doc, contextMock); expect(migratedDoc).toMatchInlineSnapshot(` Object { "attributes": Object { diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index 7c1d0568cd3d7..4f7945d6dd601 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -19,7 +19,7 @@ import { get, flow } from 'lodash'; -import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { SavedObjectMigrationFn } from 'kibana/server'; import { migrations730 } from './migrations_730'; import { migrateMatchAllQuery } from './migrate_match_all_query'; import { DashboardDoc700To720 } from '../../common'; @@ -62,7 +62,7 @@ function migrateIndexPattern(doc: DashboardDoc700To720) { doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); } -const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => { +const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => { // Set new "references" attribute doc.references = doc.references || []; @@ -111,7 +111,7 @@ export const dashboardSavedObjectTypeMigrations = { * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 * only contained the 6.7.2 migration and not the 7.0.1 migration. */ - '6.7.2': flow(migrateMatchAllQuery), - '7.0.0': flow<(doc: SavedObjectUnsanitizedDoc) => DashboardDoc700To720>(migrations700), - '7.3.0': flow(migrations730), + '6.7.2': flow>(migrateMatchAllQuery), + '7.0.0': flow>(migrations700), + '7.3.0': flow>(migrations730), }; diff --git a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts index 5b8582bf821ef..db2fbeb278802 100644 --- a/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts +++ b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts @@ -21,7 +21,7 @@ import { SavedObjectMigrationFn } from 'kibana/server'; import { get } from 'lodash'; import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; -export const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { +export const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { diff --git a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts index aa744324428a4..a58df547fa522 100644 --- a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts @@ -17,19 +17,13 @@ * under the License. */ +import { savedObjectsServiceMock } from '../../../../core/server/mocks'; import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; import { migrations730 } from './migrations_730'; import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; import { RawSavedDashboardPanel730ToLatest } from '../../common'; -const mockContext = { - log: { - warning: () => {}, - warn: () => {}, - debug: () => {}, - info: () => {}, - }, -}; +const mockContext = savedObjectsServiceMock.createMigrationContext(); test('dashboard migration 7.3.0 migrates filters to query on search source', () => { const doc: DashboardDoc700To720 = { @@ -95,7 +89,7 @@ test('dashboard migration 7.3.0 migrates filters to query on search source when }, }; - const doc700: DashboardDoc700To720 = migrations['7.0.0'](doc); + const doc700 = migrations['7.0.0'](doc, mockContext); const newDoc = migrations['7.3.0'](doc700, mockContext); const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); @@ -127,7 +121,7 @@ test('dashboard migration works when panelsJSON is missing panelIndex', () => { }, }; - const doc700: DashboardDoc700To720 = migrations['7.0.0'](doc); + const doc700 = migrations['7.0.0'](doc, mockContext); const newDoc = migrations['7.3.0'](doc700, mockContext); const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); diff --git a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts index 7a16386ea484c..c64f7361a8cf4 100644 --- a/src/plugins/data/server/saved_objects/index_pattern_migrations.ts +++ b/src/plugins/data/server/saved_objects/index_pattern_migrations.ts @@ -20,7 +20,7 @@ import { flow, omit } from 'lodash'; import { SavedObjectMigrationFn } from 'kibana/server'; -const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => ({ +const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => ({ ...doc, attributes: { ...doc.attributes, @@ -29,7 +29,7 @@ const migrateAttributeTypeAndAttributeTypeMeta: SavedObjectMigrationFn = doc => }, }); -const migrateSubTypeAndParentFieldProperties: SavedObjectMigrationFn = doc => { +const migrateSubTypeAndParentFieldProperties: SavedObjectMigrationFn = doc => { if (!doc.attributes.fields) return doc; const fieldsString = doc.attributes.fields; diff --git a/src/plugins/data/server/saved_objects/search_migrations.ts b/src/plugins/data/server/saved_objects/search_migrations.ts index 45fa5e11e2a3d..c8ded51193c92 100644 --- a/src/plugins/data/server/saved_objects/search_migrations.ts +++ b/src/plugins/data/server/saved_objects/search_migrations.ts @@ -21,7 +21,7 @@ import { flow, get } from 'lodash'; import { SavedObjectMigrationFn } from 'kibana/server'; import { DEFAULT_QUERY_LANGUAGE } from '../../common'; -const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { +const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { @@ -55,7 +55,7 @@ const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { return doc; }; -const migrateIndexPattern: SavedObjectMigrationFn = doc => { +const migrateIndexPattern: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (typeof searchSourceJSON !== 'string') { return doc; @@ -97,13 +97,13 @@ const migrateIndexPattern: SavedObjectMigrationFn = doc => { return doc; }; -const setNewReferences: SavedObjectMigrationFn = (doc, context) => { +const setNewReferences: SavedObjectMigrationFn = (doc, context) => { doc.references = doc.references || []; // Migrate index pattern return migrateIndexPattern(doc, context); }; -const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => { +const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => { const sort = get(doc, 'attributes.sort'); if (!sort) return doc; @@ -122,7 +122,7 @@ const migrateSearchSortToNestedArray: SavedObjectMigrationFn = doc => { }; export const searchSavedObjectTypeMigrations = { - '6.7.2': flow(migrateMatchAllQuery), - '7.0.0': flow(setNewReferences), - '7.4.0': flow(migrateSearchSortToNestedArray), + '6.7.2': flow>(migrateMatchAllQuery), + '7.0.0': flow>(setNewReferences), + '7.4.0': flow>(migrateSearchSortToNestedArray), }; diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts index 34922976f22ff..1e5508b44ee0e 100644 --- a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts +++ b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts @@ -20,7 +20,7 @@ import { flow } from 'lodash'; import { SavedObjectMigrationFn, SavedObjectsType } from 'kibana/server'; -const resetCount: SavedObjectMigrationFn = doc => ({ +const resetCount: SavedObjectMigrationFn = doc => ({ ...doc, attributes: { ...doc.attributes, diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index 94473e35a942d..f6455d0c1e43f 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -21,7 +21,7 @@ import { SavedObjectMigrationFn } from 'kibana/server'; import { cloneDeep, get, omit, has, flow } from 'lodash'; import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; -const migrateIndexPattern: SavedObjectMigrationFn = doc => { +const migrateIndexPattern: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (typeof searchSourceJSON !== 'string') { return doc; @@ -64,7 +64,7 @@ const migrateIndexPattern: SavedObjectMigrationFn = doc => { }; // [TSVB] Migrate percentile-rank aggregation (value -> values) -const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => { +const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -100,7 +100,7 @@ const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => { }; // [TSVB] Remove stale opperator key -const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => { +const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -132,7 +132,7 @@ const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => { }; // Migrate date histogram aggregation (remove customInterval) -const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => { +const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -174,7 +174,7 @@ const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => { return doc; }; -const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => { +const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { let visState; @@ -206,7 +206,7 @@ const removeDateHistogramTimeZones: SavedObjectMigrationFn = doc => { // migrate gauge verticalSplit to alignment // https://github.com/elastic/kibana/issues/34636 -const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logger) => { +const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logger) => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -241,7 +241,7 @@ const migrateGaugeVerticalSplitToAlignment: SavedObjectMigrationFn = (doc, logge Path to the series array is thus: attributes.visState. */ -const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) => { +const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) => { // Migrate filters // If any filters exist and they are a string, we assume it to be lucene and transform the filter into an object accordingly const newDoc = cloneDeep(doc); @@ -325,7 +325,7 @@ const transformFilterStringToQueryObject: SavedObjectMigrationFn = (doc, logger) return newDoc; }; -const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => { +const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => { // Migrate split_filters in TSVB objects that weren't migrated in 7.3 // If any filters exist and they are a string, we assume them to be lucene syntax and transform the filter into an object accordingly const newDoc = cloneDeep(doc); @@ -370,7 +370,7 @@ const transformSplitFiltersStringToQueryObject: SavedObjectMigrationFn = doc => return newDoc; }; -const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => { +const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -402,7 +402,7 @@ const migrateFiltersAggQuery: SavedObjectMigrationFn = doc => { return doc; }; -const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => { +const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -450,7 +450,7 @@ const replaceMovAvgToMovFn: SavedObjectMigrationFn = (doc, logger) => { return doc; }; -const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger) => { +const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger) => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -483,12 +483,12 @@ const migrateFiltersAggQueryStringQueries: SavedObjectMigrationFn = (doc, logger return doc; }; -const addDocReferences: SavedObjectMigrationFn = doc => ({ +const addDocReferences: SavedObjectMigrationFn = doc => ({ ...doc, references: doc.references || [], }); -const migrateSavedSearch: SavedObjectMigrationFn = doc => { +const migrateSavedSearch: SavedObjectMigrationFn = doc => { const savedSearchId = get(doc, 'attributes.savedSearchId'); if (savedSearchId && doc.references) { @@ -505,7 +505,7 @@ const migrateSavedSearch: SavedObjectMigrationFn = doc => { return doc; }; -const migrateControls: SavedObjectMigrationFn = doc => { +const migrateControls: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); if (visStateJSON) { @@ -536,7 +536,7 @@ const migrateControls: SavedObjectMigrationFn = doc => { return doc; }; -const migrateTableSplits: SavedObjectMigrationFn = doc => { +const migrateTableSplits: SavedObjectMigrationFn = doc => { try { const visState = JSON.parse(doc.attributes.visState); if (get(visState, 'type') !== 'table') { @@ -572,7 +572,7 @@ const migrateTableSplits: SavedObjectMigrationFn = doc => { } }; -const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { +const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); if (searchSourceJSON) { @@ -606,7 +606,7 @@ const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { }; // [TSVB] Default color palette is changing, keep the default for older viz -const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = doc => { +const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); let visState; @@ -649,27 +649,30 @@ export const visualizationSavedObjectTypeMigrations = { * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 * only contained the 6.7.2 migration and not the 7.0.1 migration. */ - '6.7.2': flow(migrateMatchAllQuery, removeDateHistogramTimeZones), - '7.0.0': flow( + '6.7.2': flow>( + migrateMatchAllQuery, + removeDateHistogramTimeZones + ), + '7.0.0': flow>( addDocReferences, migrateIndexPattern, migrateSavedSearch, migrateControls, migrateTableSplits ), - '7.0.1': flow(removeDateHistogramTimeZones), - '7.2.0': flow( + '7.0.1': flow>(removeDateHistogramTimeZones), + '7.2.0': flow>( migratePercentileRankAggregation, migrateDateHistogramAggregation ), - '7.3.0': flow( + '7.3.0': flow>( migrateGaugeVerticalSplitToAlignment, transformFilterStringToQueryObject, migrateFiltersAggQuery, replaceMovAvgToMovFn ), - '7.3.1': flow(migrateFiltersAggQueryStringQueries), - '7.4.2': flow(transformSplitFiltersStringToQueryObject), - '7.7.0': flow(migrateOperatorKeyTypo), - '7.8.0': flow(migrateTsvbDefaultColorPalettes), + '7.3.1': flow>(migrateFiltersAggQueryStringQueries), + '7.4.2': flow>(transformSplitFiltersStringToQueryObject), + '7.7.0': flow>(migrateOperatorKeyTypo), + '7.8.0': flow>(migrateTsvbDefaultColorPalettes), }; diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.ts b/x-pack/plugins/graph/server/saved_objects/migrations.ts index e77d2ea0fb7c9..beb31d548c670 100644 --- a/x-pack/plugins/graph/server/saved_objects/migrations.ts +++ b/x-pack/plugins/graph/server/saved_objects/migrations.ts @@ -8,7 +8,7 @@ import { get } from 'lodash'; import { SavedObjectUnsanitizedDoc } from 'kibana/server'; export const graphMigrations = { - '7.0.0': (doc: SavedObjectUnsanitizedDoc) => { + '7.0.0': (doc: SavedObjectUnsanitizedDoc) => { // Set new "references" attribute doc.references = doc.references || []; // Migrate index pattern diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index 51fcd3b6198c3..583fba1a4a999 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -19,7 +19,8 @@ interface XYLayerPre77 { * Removes the `lens_auto_date` subexpression from a stored expression * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"} */ -const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { const expression: string = doc.attributes?.expression; try { const ast = fromExpression(expression); @@ -73,7 +74,8 @@ const removeLensAutoDate: SavedObjectMigrationFn = (doc, context) => { /** * Adds missing timeField arguments to esaggs in the Lens expression */ -const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { const expression: string = doc.attributes?.expression; try { @@ -131,7 +133,8 @@ const addTimeFieldToEsaggs: SavedObjectMigrationFn = (doc, context) => { } }; -export const migrations: Record = { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const migrations: Record> = { '7.7.0': doc => { const newDoc = cloneDeep(doc); if (newDoc.attributes?.visualizationType === 'lnsXY') { diff --git a/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts index b063404f68e4f..65a810ff94a1f 100644 --- a/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts +++ b/x-pack/plugins/spaces/server/saved_objects/migrations/migrate_6x.ts @@ -6,7 +6,7 @@ import { SavedObjectMigrationFn } from 'src/core/server'; -export const migrateToKibana660: SavedObjectMigrationFn = doc => { +export const migrateToKibana660: SavedObjectMigrationFn = doc => { if (!doc.attributes.hasOwnProperty('disabledFeatures')) { doc.attributes.disabledFeatures = []; } From dcc4081bd81d080776f019e98232fe743bd226d2 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 30 Apr 2020 10:34:32 +0200 Subject: [PATCH 024/122] Dashboard url generator to preserve saved filters from destination dashboard (#64767) --- src/plugins/dashboard/public/plugin.tsx | 12 +- .../dashboard/public/url_generator.test.ts | 207 +++++++++++++++++- src/plugins/dashboard/public/url_generator.ts | 39 +++- 3 files changed, 244 insertions(+), 14 deletions(-) diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 5f6b67ee6ad20..7de054f2eaa9c 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -141,10 +141,14 @@ export class DashboardPlugin if (share) { share.urlGenerators.registerUrlGenerator( - createDirectAccessDashboardLinkGenerator(async () => ({ - appBasePath: (await startServices)[0].application.getUrlForApp('dashboard'), - useHashedUrl: (await startServices)[0].uiSettings.get('state:storeInSessionStorage'), - })) + createDirectAccessDashboardLinkGenerator(async () => { + const [coreStart, , selfStart] = await startServices; + return { + appBasePath: coreStart.application.getUrlForApp('dashboard'), + useHashedUrl: coreStart.uiSettings.get('state:storeInSessionStorage'), + savedDashboardLoader: selfStart.getSavedDashboardLoader(), + }; + }) ); } diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index d48aacc1d8c1e..248a3f991d6cb 100644 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ b/src/plugins/dashboard/public/url_generator.test.ts @@ -21,10 +21,33 @@ import { createDirectAccessDashboardLinkGenerator } from './url_generator'; import { hashedItemStore } from '../../kibana_utils/public'; // eslint-disable-next-line import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; -import { esFilters } from '../../data/public'; +import { esFilters, Filter } from '../../data/public'; +import { SavedObjectLoader } from '../../saved_objects/public'; const APP_BASE_PATH: string = 'xyz/app/kibana'; +const createMockDashboardLoader = ( + dashboardToFilters: { + [dashboardId: string]: () => Filter[]; + } = {} +) => { + return { + get: async (dashboardId: string) => { + return { + searchSource: { + getField: (field: string) => { + if (field === 'filter') + return dashboardToFilters[dashboardId] ? dashboardToFilters[dashboardId]() : []; + throw new Error( + `createMockDashboardLoader > searchSource > getField > ${field} is not mocked` + ); + }, + }, + }; + }, + } as SavedObjectLoader; +}; + describe('dashboard url generator', () => { beforeEach(() => { // @ts-ignore @@ -33,7 +56,11 @@ describe('dashboard url generator', () => { test('creates a link to a saved dashboard', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({}); expect(url).toMatchInlineSnapshot(`"xyz/app/kibana#/dashboard?_a=()&_g=()"`); @@ -41,7 +68,11 @@ describe('dashboard url generator', () => { test('creates a link with global time range set up', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -53,7 +84,11 @@ describe('dashboard url generator', () => { test('creates a link with filters, time range, refresh interval and query to a saved object', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -89,7 +124,11 @@ describe('dashboard url generator', () => { test('if no useHash setting is given, uses the one was start services', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: true }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: true, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -99,7 +138,11 @@ describe('dashboard url generator', () => { test('can override a false useHash ui setting', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -110,7 +153,11 @@ describe('dashboard url generator', () => { test('can override a true useHash ui setting', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => - Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: true }) + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: true, + savedDashboardLoader: createMockDashboardLoader(), + }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, @@ -118,4 +165,150 @@ describe('dashboard url generator', () => { }); expect(url.indexOf('relative')).toBeGreaterThan(1); }); + + describe('preserving saved filters', () => { + const savedFilter1 = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'savedfilter1' }, + }; + + const savedFilter2 = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'savedfilter2' }, + }; + + const appliedFilter = { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'appliedfilter' }, + }; + + test('attaches filters from destination dashboard', async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => [savedFilter1], + ['dashboard2']: () => [savedFilter2], + }), + }) + ); + + const urlToDashboard1 = await generator.createUrl!({ + dashboardId: 'dashboard1', + filters: [appliedFilter], + }); + + expect(urlToDashboard1).toEqual(expect.stringContaining('query:savedfilter1')); + expect(urlToDashboard1).toEqual(expect.stringContaining('query:appliedfilter')); + + const urlToDashboard2 = await generator.createUrl!({ + dashboardId: 'dashboard2', + filters: [appliedFilter], + }); + + expect(urlToDashboard2).toEqual(expect.stringContaining('query:savedfilter2')); + expect(urlToDashboard2).toEqual(expect.stringContaining('query:appliedfilter')); + }); + + test("doesn't fail if can't retrieve filters from destination dashboard", async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => { + throw new Error('Not found'); + }, + }), + }) + ); + + const url = await generator.createUrl!({ + dashboardId: 'dashboard1', + filters: [appliedFilter], + }); + + expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); + expect(url).toEqual(expect.stringContaining('query:appliedfilter')); + }); + + test('can enforce empty filters', async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => [savedFilter1], + }), + }) + ); + + const url = await generator.createUrl!({ + dashboardId: 'dashboard1', + filters: [], + preserveSavedFilters: false, + }); + + expect(url).not.toEqual(expect.stringContaining('query:savedfilter1')); + expect(url).not.toEqual(expect.stringContaining('query:appliedfilter')); + expect(url).toMatchInlineSnapshot( + `"xyz/app/kibana#/dashboard/dashboard1?_a=(filters:!())&_g=(filters:!())"` + ); + }); + + test('no filters in result url if no filters applied', async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => [savedFilter1], + }), + }) + ); + + const url = await generator.createUrl!({ + dashboardId: 'dashboard1', + }); + expect(url).not.toEqual(expect.stringContaining('filters')); + expect(url).toMatchInlineSnapshot(`"xyz/app/kibana#/dashboard/dashboard1?_a=()&_g=()"`); + }); + + test('can turn off preserving filters', async () => { + const generator = createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader({ + ['dashboard1']: () => [savedFilter1], + }), + }) + ); + const urlWithPreservedFiltersTurnedOff = await generator.createUrl!({ + dashboardId: 'dashboard1', + filters: [appliedFilter], + preserveSavedFilters: false, + }); + + expect(urlWithPreservedFiltersTurnedOff).not.toEqual( + expect.stringContaining('query:savedfilter1') + ); + expect(urlWithPreservedFiltersTurnedOff).toEqual( + expect.stringContaining('query:appliedfilter') + ); + }); + }); }); diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts index 0fdf395e75bca..6f121ceb2d373 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -27,6 +27,7 @@ import { } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; import { UrlGeneratorsDefinition, UrlGeneratorState } from '../../share/public'; +import { SavedObjectLoader } from '../../saved_objects/public'; export const STATE_STORAGE_KEY = '_a'; export const GLOBAL_STATE_STORAGE_KEY = '_g'; @@ -64,10 +65,22 @@ export type DashboardAppLinkGeneratorState = UrlGeneratorState<{ * whether to hash the data in the url to avoid url length issues. */ useHash?: boolean; + + /** + * When `true` filters from saved filters from destination dashboard as merged with applied filters + * When `false` applied filters take precedence and override saved filters + * + * true is default + */ + preserveSavedFilters?: boolean; }>; export const createDirectAccessDashboardLinkGenerator = ( - getStartServices: () => Promise<{ appBasePath: string; useHashedUrl: boolean }> + getStartServices: () => Promise<{ + appBasePath: string; + useHashedUrl: boolean; + savedDashboardLoader: SavedObjectLoader; + }> ): UrlGeneratorsDefinition => ({ id: DASHBOARD_APP_URL_GENERATOR, createUrl: async state => { @@ -76,6 +89,19 @@ export const createDirectAccessDashboardLinkGenerator = ( const appBasePath = startServices.appBasePath; const hash = state.dashboardId ? `dashboard/${state.dashboardId}` : `dashboard`; + const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise => { + if (state.preserveSavedFilters === false) return []; + if (!state.dashboardId) return []; + try { + const dashboard = await startServices.savedDashboardLoader.get(state.dashboardId); + return dashboard?.searchSource?.getField('filter') ?? []; + } catch (e) { + // in case dashboard is missing, built the url without those filters + // dashboard app will handle redirect to landing page with toast message + return []; + } + }; + const cleanEmptyKeys = (stateObj: Record) => { Object.keys(stateObj).forEach(key => { if (stateObj[key] === undefined) { @@ -85,11 +111,18 @@ export const createDirectAccessDashboardLinkGenerator = ( return stateObj; }; + // leave filters `undefined` if no filters was applied + // in this case dashboard will restore saved filters on its own + const filters = state.filters && [ + ...(await getSavedFiltersFromDestinationDashboardIfNeeded()), + ...state.filters, + ]; + const appStateUrl = setStateToKbnUrl( STATE_STORAGE_KEY, cleanEmptyKeys({ query: state.query, - filters: state.filters?.filter(f => !esFilters.isFilterPinned(f)), + filters: filters?.filter(f => !esFilters.isFilterPinned(f)), }), { useHash }, `${appBasePath}#/${hash}` @@ -99,7 +132,7 @@ export const createDirectAccessDashboardLinkGenerator = ( GLOBAL_STATE_STORAGE_KEY, cleanEmptyKeys({ time: state.timeRange, - filters: state.filters?.filter(f => esFilters.isFilterPinned(f)), + filters: filters?.filter(f => esFilters.isFilterPinned(f)), refreshInterval: state.refreshInterval, }), { useHash }, From 21bc46f5af1ede7dd209e5a58a224edc37d4b986 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 30 Apr 2020 11:07:14 +0100 Subject: [PATCH 025/122] [ML] Disable data frame anaylics clone button based on permission (#64830) --- .../components/analytics_list/action_clone.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index 8c65af1d92959..cc75ddbe08cfb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -18,6 +18,7 @@ import { } from '../../hooks/use_create_analytics_form'; import { State } from '../../hooks/use_create_analytics_form/state'; import { DataFrameAnalyticsListRow } from './common'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; interface PropDefinition { /** @@ -322,6 +323,8 @@ interface CloneActionProps { * to support EuiContext with a valid DOM structure without nested buttons. */ export const CloneAction: FC = ({ createAnalyticsForm, item }) => { + const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics'); + const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', { defaultMessage: 'Clone job', }); @@ -338,6 +341,7 @@ export const CloneAction: FC = ({ createAnalyticsForm, item }) iconType="copy" onClick={onClick} aria-label={buttonText} + disabled={canCreateDataFrameAnalytics === false} > {buttonText}
From 3c56a8e296b4b08c9f73a4924b189b67518a3441 Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Thu, 30 Apr 2020 12:43:34 +0200 Subject: [PATCH 026/122] [EPM] Handle constant_keyword type in KB index patterns and ES index templates (#64876) * Unit-test constant_keyword mapping generation * Treat constant_keyword as string in kibana index patterns --- .../elasticsearch/template/template.test.ts | 18 ++++++++++++++++++ .../epm/kibana/index_pattern/install.test.ts | 2 ++ .../epm/kibana/index_pattern/install.ts | 1 + 3 files changed, 21 insertions(+) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts index 3679c577ee571..25180244b0214 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts @@ -309,3 +309,21 @@ test('tests processing object field with property, reverse order', () => { const mappings = generateMappings(processedFields); expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldWithPropertyReversedMapping)); }); + +test('tests constant_keyword field type handling', () => { + const constantKeywordLiteralYaml = ` +- name: constantKeyword + type: constant_keyword + `; + const constantKeywordMapping = { + properties: { + constantKeyword: { + type: 'constant_keyword', + }, + }, + }; + const fields: Field[] = safeLoad(constantKeywordLiteralYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(JSON.stringify(mappings)).toEqual(JSON.stringify(constantKeywordMapping)); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts index bc1694348b4c2..f1660fbc01591 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.test.ts @@ -150,6 +150,7 @@ describe('creating index patterns from yaml fields', () => { { fields: [{ name: 'testField', type: 'text' }], expect: 'string' }, { fields: [{ name: 'testField', type: 'date' }], expect: 'date' }, { fields: [{ name: 'testField', type: 'geo_point' }], expect: 'geo_point' }, + { fields: [{ name: 'testField', type: 'constant_keyword' }], expect: 'string' }, ]; tests.forEach(test => { @@ -191,6 +192,7 @@ describe('creating index patterns from yaml fields', () => { attr: 'aggregatable', }, { fields: [{ name, type: 'keyword' }], expect: true, attr: 'aggregatable' }, + { fields: [{ name, type: 'constant_keyword' }], expect: true, attr: 'aggregatable' }, { fields: [{ name, type: 'text', aggregatable: true }], expect: false, attr: 'aggregatable' }, { fields: [{ name, type: 'text' }], expect: false, attr: 'aggregatable' }, { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index 05e64c6565dc6..ec657820a2225 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -47,6 +47,7 @@ const typeMap: TypeMap = { date: 'date', ip: 'ip', boolean: 'boolean', + constant_keyword: 'string', }; export interface IndexPatternField { From b140ad7194338610b791750001730279a4806ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 30 Apr 2020 08:56:15 -0400 Subject: [PATCH 027/122] Remove edit alert button from alerts list (#64643) * Remove edit alert button from alerts list * Remove unused code * Cleanup translation files Co-authored-by: Elastic Machine --- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../sections/alert_form/alert_form.test.tsx | 20 ++ .../components/alerts_list.test.tsx | 8 - .../alerts_list/components/alerts_list.tsx | 38 +--- .../apps/triggers_actions_ui/alerts.ts | 187 ------------------ 6 files changed, 21 insertions(+), 234 deletions(-) diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 81dc44f3a4cb4..a3011ab5bdfa9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15899,7 +15899,6 @@ "xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "アクションタイプ", "xpack.triggersActionsUI.sections.alertsList.addActionButtonLabel": "アラートの作成", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "タイプ", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editLinkTitle": "編集", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "次の間隔で実行", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名前", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "タグ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e06edb45de8fa..e373d05a7d851 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15904,7 +15904,6 @@ "xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "操作类型", "xpack.triggersActionsUI.sections.alertsList.addActionButtonLabel": "创建告警", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "类型", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editLinkTitle": "编辑", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.intervalTitle": "运行间隔", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名称", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "标记", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx index 72c22f46f217e..8406987e4ed9d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.test.tsx @@ -190,5 +190,25 @@ describe('alert_form', () => { const alertTypeSelectOptions = wrapper.find('[data-test-subj="selectedAlertTypeTitle"]'); expect(alertTypeSelectOptions.exists()).toBeTruthy(); }); + + it('should update throttle value', async () => { + const newThrottle = 17; + await setup(); + const throttleField = wrapper.find('[data-test-subj="throttleInput"]'); + expect(throttleField.exists()).toBeTruthy(); + throttleField.at(1).simulate('change', { target: { value: newThrottle.toString() } }); + const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]'); + expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle); + }); + + it('should unset throttle value', async () => { + const newThrottle = ''; + await setup(); + const throttleField = wrapper.find('[data-test-subj="throttleInput"]'); + expect(throttleField.exists()).toBeTruthy(); + throttleField.at(1).simulate('change', { target: { value: newThrottle } }); + const throttleFieldAfterUpdate = wrapper.find('[data-test-subj="throttleInput"]'); + expect(throttleFieldAfterUpdate.at(1).prop('value')).toEqual(newThrottle); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 66aa02e1930a3..a51ebc3126785 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -245,10 +245,6 @@ describe('alerts_list component with items', () => { expect(wrapper.find('EuiBasicTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); }); - it('renders edit button for registered alert types', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="alertsTableCell-editLink"]').length).toBeGreaterThan(0); - }); }); describe('alerts_list component empty with show only capability', () => { @@ -442,8 +438,4 @@ describe('alerts_list with show only capability', () => { expect(wrapper.find('EuiTableRow')).toHaveLength(2); // TODO: check delete button }); - it('not renders edit button for non registered alert types', async () => { - await setup(); - expect(wrapper.find('[data-test-subj="alertsTableCell-editLink"]').length).toBe(0); - }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 1103d7c3921a7..2d9cfcdbda89f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -24,7 +24,7 @@ import { isEmpty } from 'lodash'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { useAppDependencies } from '../../../app_context'; import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types'; -import { AlertAdd, AlertEdit } from '../../alert_form'; +import { AlertAdd } from '../../alert_form'; import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons'; import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; @@ -85,8 +85,6 @@ export const AlertsList: React.FunctionComponent = () => { data: [], totalItemCount: 0, }); - const [editedAlertItem, setEditedAlertItem] = useState(undefined); - const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [alertsToDelete, setAlertsToDelete] = useState([]); useEffect(() => { @@ -162,11 +160,6 @@ export const AlertsList: React.FunctionComponent = () => { } } - async function editItem(alertTableItem: AlertTableItem) { - setEditedAlertItem(alertTableItem); - setEditFlyoutVisibility(true); - } - const alertsTableColumns = [ { field: 'name', @@ -219,27 +212,6 @@ export const AlertsList: React.FunctionComponent = () => { truncateText: false, 'data-test-subj': 'alertsTableCell-interval', }, - { - name: '', - width: '50px', - render(item: AlertTableItem) { - if (!canSave || !alertTypeRegistry.has(item.alertTypeId)) { - return; - } - return ( - editItem(item)} - > - - - ); - }, - }, { name: '', width: '40px', @@ -453,14 +425,6 @@ export const AlertsList: React.FunctionComponent = () => { addFlyoutVisible={alertFlyoutVisible} setAddFlyoutVisibility={setAlertFlyoutVisibility} /> - {editFlyoutVisible && editedAlertItem ? ( - - ) : null} ); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 597f1ad9119b0..9e99c60b4dcb7 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -126,193 +126,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ]); }); - it('should edit an alert', async () => { - const createdAlert = await createAlert({ - alertTypeId: '.index-threshold', - name: generateUniqueKey(), - params: { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', - }, - }); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - const editLink = await testSubjects.findAll('alertsTableCell-editLink'); - await editLink[0].click(); - - const updatedAlertName = `Changed Alert Name ${generateUniqueKey()}`; - await testSubjects.setValue('alertNameInput', updatedAlertName, { clearWithKeyboard: true }); - - await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); - - const toastTitle = await pageObjects.common.closeToast(); - expect(toastTitle).to.eql(`Updated '${updatedAlertName}'`); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(updatedAlertName); - - const searchResultsAfterEdit = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResultsAfterEdit).to.eql([ - { - name: updatedAlertName, - tagsText: 'foo, bar', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - }); - - it('should set an alert throttle', async () => { - const alertName = `edit throttle ${generateUniqueKey()}`; - const createdAlert = await createAlert({ - alertTypeId: '.index-threshold', - name: alertName, - params: { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', - }, - }); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - - const editLink = await testSubjects.findAll('alertsTableCell-editLink'); - await editLink[0].click(); - - await testSubjects.setValue('throttleInput', '1', { clearWithKeyboard: true }); - - await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); - - expect(await pageObjects.common.closeToast()).to.eql(`Updated '${createdAlert.name}'`); - - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - await (await testSubjects.findAll('alertsTableCell-editLink'))[0].click(); - const throttleInput = await testSubjects.find('throttleInput'); - expect(await throttleInput.getAttribute('value')).to.eql('1'); - }); - - it('should unset an alert throttle', async () => { - const alertName = `edit throttle ${generateUniqueKey()}`; - const createdAlert = await createAlert({ - alertTypeId: '.index-threshold', - name: alertName, - throttle: '10m', - params: { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', - }, - }); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Index threshold', - interval: '1m', - }, - ]); - - const editLink = await testSubjects.findAll('alertsTableCell-editLink'); - await editLink[0].click(); - - const throttleInputToUnsetValue = await testSubjects.find('throttleInput'); - - expect(await throttleInputToUnsetValue.getAttribute('value')).to.eql('10'); - await throttleInputToUnsetValue.click(); - await throttleInputToUnsetValue.clearValueWithKeyboard(); - - expect(await throttleInputToUnsetValue.getAttribute('value')).to.eql(''); - - await find.clickByCssSelector('[data-test-subj="saveEditedAlertButton"]:not(disabled)'); - - expect(await pageObjects.common.closeToast()).to.eql(`Updated '${createdAlert.name}'`); - - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - await (await testSubjects.findAll('alertsTableCell-editLink'))[0].click(); - const throttleInput = await testSubjects.find('throttleInput'); - expect(await throttleInput.getAttribute('value')).to.eql(''); - }); - - it('should reset alert when canceling an edit', async () => { - const createdAlert = await createAlert({ - alertTypeId: '.index-threshold', - name: generateUniqueKey(), - params: { - aggType: 'count', - termSize: 5, - thresholdComparator: '>', - timeWindowSize: 5, - timeWindowUnit: 'm', - groupBy: 'all', - threshold: [1000, 5000], - index: ['.kibana_1'], - timeField: 'alert', - }, - }); - await pageObjects.common.navigateToApp('triggersActions'); - await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); - - const editLink = await testSubjects.findAll('alertsTableCell-editLink'); - await editLink[0].click(); - - const updatedAlertName = `Changed Alert Name ${generateUniqueKey()}`; - await testSubjects.setValue('alertNameInput', updatedAlertName); - - await testSubjects.click('cancelSaveEditedAlertButton'); - await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]'); - - const editLinkPostCancel = await testSubjects.findAll('alertsTableCell-editLink'); - await editLinkPostCancel[0].click(); - - const nameInputAfterCancel = await testSubjects.find('alertNameInput'); - const textAfterCancel = await nameInputAfterCancel.getAttribute('value'); - expect(textAfterCancel).to.eql(createdAlert.name); - }); - it('should search for tags', async () => { const createdAlert = await createAlert(); await pageObjects.common.navigateToApp('triggersActions'); From 497c5da763be143d65106e292e9d45bc7496c7b9 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 30 Apr 2020 14:07:32 +0100 Subject: [PATCH 028/122] [ML] Moving get filters capability to admin (#64879) * [ML] Moving get filters capability to admin * updating test --- x-pack/plugins/ml/common/types/capabilities.ts | 4 ++-- .../lib/capabilities/check_capabilities.test.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index da5fd3ac25209..572217ce16eee 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -15,8 +15,6 @@ export const userMlCapabilities = { canGetCalendars: false, // File Data Visualizer canFindFileStructure: false, - // Filters - canGetFilters: false, // Data Frame Analytics canGetDataFrameAnalytics: false, // Annotations @@ -38,6 +36,8 @@ export const adminMlCapabilities = { canStartStopDatafeed: false, canUpdateDatafeed: false, canPreviewDatafeed: false, + // Filters + canGetFilters: false, // Calendars canCreateCalendar: false, canDeleteCalendar: false, diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts index b6e95ae8373ee..746c9da47d0ad 100644 --- a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -64,7 +64,6 @@ describe('check_capabilities', () => { expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canGetAnnotations).toBe(true); expect(capabilities.canCreateAnnotation).toBe(true); @@ -81,6 +80,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); expect(capabilities.canCreateFilter).toBe(false); @@ -113,7 +113,6 @@ describe('check_capabilities', () => { expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canGetAnnotations).toBe(true); expect(capabilities.canCreateAnnotation).toBe(true); @@ -130,6 +129,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteDatafeed).toBe(true); expect(capabilities.canUpdateDatafeed).toBe(true); expect(capabilities.canPreviewDatafeed).toBe(true); + expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canCreateCalendar).toBe(true); expect(capabilities.canDeleteCalendar).toBe(true); expect(capabilities.canCreateFilter).toBe(true); @@ -162,7 +162,6 @@ describe('check_capabilities', () => { expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canGetAnnotations).toBe(true); expect(capabilities.canCreateAnnotation).toBe(false); @@ -177,6 +176,7 @@ describe('check_capabilities', () => { expect(capabilities.canUpdateJob).toBe(false); expect(capabilities.canCreateDatafeed).toBe(false); expect(capabilities.canDeleteDatafeed).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); @@ -211,7 +211,6 @@ describe('check_capabilities', () => { expect(capabilities.canGetDatafeeds).toBe(true); expect(capabilities.canGetCalendars).toBe(true); expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetFilters).toBe(true); expect(capabilities.canGetDataFrameAnalytics).toBe(true); expect(capabilities.canGetAnnotations).toBe(true); expect(capabilities.canCreateAnnotation).toBe(false); @@ -228,6 +227,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); expect(capabilities.canCreateFilter).toBe(false); @@ -260,7 +260,6 @@ describe('check_capabilities', () => { expect(capabilities.canGetDatafeeds).toBe(false); expect(capabilities.canGetCalendars).toBe(false); expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canGetDataFrameAnalytics).toBe(false); expect(capabilities.canGetAnnotations).toBe(false); expect(capabilities.canCreateAnnotation).toBe(false); @@ -277,6 +276,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); expect(capabilities.canCreateFilter).toBe(false); @@ -311,7 +311,6 @@ describe('check_capabilities', () => { expect(capabilities.canGetDatafeeds).toBe(false); expect(capabilities.canGetCalendars).toBe(false); expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canGetDataFrameAnalytics).toBe(false); expect(capabilities.canGetAnnotations).toBe(false); expect(capabilities.canCreateAnnotation).toBe(false); @@ -328,6 +327,7 @@ describe('check_capabilities', () => { expect(capabilities.canDeleteDatafeed).toBe(false); expect(capabilities.canUpdateDatafeed).toBe(false); expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetFilters).toBe(false); expect(capabilities.canCreateCalendar).toBe(false); expect(capabilities.canDeleteCalendar).toBe(false); expect(capabilities.canCreateFilter).toBe(false); From 60ef51b9122ca6f60590da95f4f6bc75d9f48073 Mon Sep 17 00:00:00 2001 From: Henry Harding Date: Thu, 30 Apr 2020 09:28:12 -0400 Subject: [PATCH 029/122] Feature/send feedback link (#64845) * use fixed table layout * add alpha messaging flyout * Added alpha badge + data streams link * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx Co-Authored-By: Jen Huang * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx Co-Authored-By: Jen Huang * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_flyout.tsx Co-Authored-By: Jen Huang * Update x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx Co-Authored-By: Jen Huang * remove small tags * change messaging from alpha to experimental * add period * remove unused imports * fixed i18n ids * add send feedback link to header * rename "ingest management" to "ingest manager" Co-authored-by: Jen Huang Co-authored-by: Elastic Machine --- .../components/settings_flyout.tsx | 2 +- .../ingest_manager/layouts/default.tsx | 28 +++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx index 92146e9ee5679..9863463e68a01 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx @@ -209,7 +209,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => {

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 10245e73520f7..4a9cfe02b74ac 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -96,12 +96,28 @@ export const DefaultLayout: React.FunctionComponent = ({ section, childre - setIsSettingsFlyoutOpen(true)}> - - + + + + + + + + setIsSettingsFlyoutOpen(true)}> + + + + From 1736005e5e733212f508fa36ee6d348a00bf6e93 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 30 Apr 2020 15:45:52 +0200 Subject: [PATCH 030/122] [Uptime] Remove hard coded value for monitor states histograms (#64396) --- x-pack/plugins/uptime/common/constants/index.ts | 2 +- x-pack/plugins/uptime/common/constants/query.ts | 7 ------- .../server/lib/requests/search/enrich_monitor_groups.ts | 4 ++-- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/uptime/common/constants/index.ts b/x-pack/plugins/uptime/common/constants/index.ts index 72d498056d6b3..00baa39044a55 100644 --- a/x-pack/plugins/uptime/common/constants/index.ts +++ b/x-pack/plugins/uptime/common/constants/index.ts @@ -11,6 +11,6 @@ export { CONTEXT_DEFAULTS } from './context_defaults'; export * from './capabilities'; export * from './settings_defaults'; export { PLUGIN } from './plugin'; -export { QUERY, STATES } from './query'; +export { QUERY } from './query'; export * from './ui'; export * from './rest_api'; diff --git a/x-pack/plugins/uptime/common/constants/query.ts b/x-pack/plugins/uptime/common/constants/query.ts index d728f114aae76..21574f1d8b27e 100644 --- a/x-pack/plugins/uptime/common/constants/query.ts +++ b/x-pack/plugins/uptime/common/constants/query.ts @@ -25,10 +25,3 @@ export const QUERY = { 'error.type', ], }; - -export const STATES = { - // Number of results returned for a states query - LEGACY_STATES_QUERY_SIZE: 10, - // The maximum number of monitors that should be supported - MAX_MONITORS: 35000, -}; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts index d21259fad77a6..8612d71dfe939 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts @@ -6,7 +6,7 @@ import { get, sortBy } from 'lodash'; import { QueryContext } from './query_context'; -import { QUERY, STATES } from '../../../../common/constants'; +import { QUERY } from '../../../../common/constants'; import { Check, Histogram, @@ -314,7 +314,7 @@ const getHistogramForMonitors = async ( by_id: { terms: { field: 'monitor.id', - size: STATES.LEGACY_STATES_QUERY_SIZE, + size: queryContext.size, }, aggs: { histogram: { From 027c8a8d752e1f90a133ca5a6c9bd7ca969791c8 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 30 Apr 2020 09:47:43 -0400 Subject: [PATCH 031/122] [Endpoint] Remove todos, urls to issues (#64833) --- .../public/applications/endpoint/store/hosts/middleware.ts | 1 - x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts | 2 -- x-pack/plugins/endpoint/server/routes/metadata/index.ts | 1 - 3 files changed, 4 deletions(-) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts index d1b9a2cde4b31..bcfd6b96c9eb8 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts @@ -70,7 +70,6 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = core type: 'serverReturnedHostDetails', payload: response, }); - // FIXME: once we have the API implementation in place, we should call it parallel with the above api call and then dispatch this with the results of the second call dispatch({ type: 'serverReturnedHostPolicyResponse', payload: { diff --git a/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts index 92bd07a813d26..114251820ce4b 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/list/lib/index.ts @@ -60,8 +60,6 @@ export const getRequestData = async ( reqData.fromIndex = reqData.pageIndex * reqData.pageSize; } - // See: https://github.com/elastic/elasticsearch-js/issues/662 - // and https://github.com/elastic/endpoint-app-team/issues/221 if ( reqData.searchBefore !== undefined && reqData.searchBefore[0] === '' && diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts index 99dc4ac9f9e33..08950930441df 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts @@ -171,7 +171,6 @@ async function enrichHostMetadata( try { /** * Get agent status by elastic agent id if available or use the host id. - * https://github.com/elastic/endpoint-app-team/issues/354 */ if (!elasticAgentId) { From d75328c476aca4afb56090e930a735c43025e7af Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 30 Apr 2020 10:34:44 -0400 Subject: [PATCH 032/122] [Ingest] Allow aggent to send metadata compliant with ECS (#64452) --- .../common/types/models/agent.ts | 11 +++-- .../ingest_manager/components/search_bar.tsx | 1 - .../agent_details_page/components/helper.ts | 47 +++++++++++++++++++ .../components/metadata_flyout.tsx | 6 ++- .../components/metadata_form.tsx | 11 +++-- .../sections/fleet/agent_list_page/index.tsx | 2 +- .../ingest_manager/types/index.ts | 1 + .../ingest_manager/server/saved_objects.ts | 4 +- .../server/services/agents/checkin.ts | 3 +- .../server/services/agents/crud.ts | 2 +- .../server/services/agents/enroll.ts | 4 +- .../server/services/agents/saved_objects.ts | 4 +- .../server/services/agents/status.test.ts | 8 ++-- .../ingest_manager/server/types/index.tsx | 1 + .../es_archives/fleet/agents/data.json | 16 +++---- .../es_archives/fleet/agents/mappings.json | 4 +- 16 files changed, 91 insertions(+), 34 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index fcd3955f3a32f..e3ca7635fdb40 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -60,6 +60,11 @@ export interface AgentEvent { export interface AgentEventSOAttributes extends AgentEvent, SavedObjectAttributes {} +type MetadataValue = string | AgentMetadata; + +export interface AgentMetadata { + [x: string]: MetadataValue; +} interface AgentBase { type: AgentType; active: boolean; @@ -72,19 +77,17 @@ interface AgentBase { config_revision?: number | null; config_newest_revision?: number; last_checkin?: string; + user_provided_metadata: AgentMetadata; + local_metadata: AgentMetadata; } export interface Agent extends AgentBase { id: string; current_error_events: AgentEvent[]; - user_provided_metadata: Record; - local_metadata: Record; access_api_key?: string; status?: string; } export interface AgentSOAttributes extends AgentBase, SavedObjectAttributes { - user_provided_metadata: string; - local_metadata: string; current_error_events?: string; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx index 4429b9d8e6b82..1c9bd9107515d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx @@ -92,7 +92,6 @@ function useSuggestions(fieldPrefix: string, search: string) { const res = (await data.indexPatterns.getFieldsForWildcard({ pattern: INDEX_NAME, })) as IFieldType[]; - if (!data || !data.autocomplete) { throw new Error('Missing data plugin'); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts new file mode 100644 index 0000000000000..508190fef0fc2 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/helper.ts @@ -0,0 +1,47 @@ +/* + * 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 { AgentMetadata } from '../../../../types'; + +export function flattenMetadata(metadata: AgentMetadata) { + return Object.entries(metadata).reduce((acc, [key, value]) => { + if (typeof value === 'string') { + acc[key] = value; + + return acc; + } + + Object.entries(flattenMetadata(value)).forEach(([flattenedKey, flattenedValue]) => { + acc[`${key}.${flattenedKey}`] = flattenedValue; + }); + + return acc; + }, {} as { [k: string]: string }); +} +export function unflattenMetadata(flattened: { [k: string]: string }) { + const metadata: AgentMetadata = {}; + + Object.entries(flattened).forEach(([flattenedKey, flattenedValue]) => { + const keyParts = flattenedKey.split('.'); + const lastKey = keyParts.pop(); + + if (!lastKey) { + throw new Error('Invalid metadata'); + } + + let metadataPart = metadata; + keyParts.forEach(keyPart => { + if (!metadataPart[keyPart]) { + metadataPart[keyPart] = {}; + } + + metadataPart = metadataPart[keyPart] as AgentMetadata; + }); + metadataPart[lastKey] = flattenedValue; + }); + + return metadata; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx index ee43385e601c2..aa6da36f8fb6c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_flyout.tsx @@ -17,11 +17,13 @@ import { } from '@elastic/eui'; import { MetadataForm } from './metadata_form'; import { Agent } from '../../../../types'; +import { flattenMetadata } from './helper'; interface Props { agent: Agent; flyout: { hide: () => void }; } + export const AgentMetadataFlyout: React.FunctionComponent = ({ agent, flyout }) => { const mapMetadata = (obj: { [key: string]: string } | undefined) => { return Object.keys(obj || {}).map(key => ({ @@ -30,8 +32,8 @@ export const AgentMetadataFlyout: React.FunctionComponent = ({ agent, fly })); }; - const localItems = mapMetadata(agent.local_metadata); - const userProvidedItems = mapMetadata(agent.user_provided_metadata); + const localItems = mapMetadata(flattenMetadata(agent.local_metadata)); + const userProvidedItems = mapMetadata(flattenMetadata(agent.user_provided_metadata)); return ( flyout.hide()} size="s" aria-labelledby="flyoutTitle"> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx index ce28bbdc590b0..af7e8c674db4c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/metadata_form.tsx @@ -22,6 +22,7 @@ import { useAgentRefresh } from '../hooks'; import { useInput, sendRequest } from '../../../../hooks'; import { Agent } from '../../../../types'; import { agentRouteService } from '../../../../services'; +import { flattenMetadata, unflattenMetadata } from './helper'; function useAddMetadataForm(agent: Agent, done: () => void) { const refreshAgent = useAgentRefresh(); @@ -66,15 +67,17 @@ function useAddMetadataForm(agent: Agent, done: () => void) { isLoading: true, }); + const metadata = unflattenMetadata({ + ...flattenMetadata(agent.user_provided_metadata), + [keyInput.value]: valueInput.value, + }); + try { const { error } = await sendRequest({ path: agentRouteService.getUpdatePath(agent.id), method: 'put', body: JSON.stringify({ - user_provided_metadata: { - ...agent.user_provided_metadata, - [keyInput.value]: valueInput.value, - }, + user_provided_metadata: metadata, }), }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 23fe18b82468c..05264c157434e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -238,7 +238,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const columns = [ { - field: 'local_metadata.host', + field: 'local_metadata.host.hostname', name: i18n.translate('xpack.ingestManager.agentList.hostColumnTitle', { defaultMessage: 'Host', }), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 1508f4dfaa628..d6483479a3f2f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -8,6 +8,7 @@ export { entries, // Object types Agent, + AgentMetadata, AgentConfig, NewAgentConfig, AgentEvent, diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index 90fe68e61bb1b..89d8b9e173ffe 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -56,8 +56,8 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { enrolled_at: { type: 'date' }, access_api_key_id: { type: 'keyword' }, version: { type: 'keyword' }, - user_provided_metadata: { type: 'text' }, - local_metadata: { type: 'text' }, + user_provided_metadata: { type: 'flattened' }, + local_metadata: { type: 'flattened' }, config_id: { type: 'keyword' }, last_updated: { type: 'date' }, last_checkin: { type: 'date' }, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts index c96a81ed9b758..9b1565e7d74aa 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts @@ -11,6 +11,7 @@ import { AgentAction, AgentSOAttributes, AgentEventSOAttributes, + AgentMetadata, } from '../../types'; import { agentConfigService } from '../agent_config'; @@ -28,7 +29,7 @@ export async function agentCheckin( const updateData: { last_checkin: string; default_api_key?: string; - local_metadata?: string; + local_metadata?: AgentMetadata; current_error_events?: string; } = { last_checkin: new Date().toISOString(), diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index 175b92b75aca0..43fd5a3ce0ac9 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -103,7 +103,7 @@ export async function updateAgent( } ) { await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - user_provided_metadata: JSON.stringify(data.userProvidedMetatada), + user_provided_metadata: data.userProvidedMetatada, }); } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts index a34d2e03e9b3d..81afa70ecb818 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts @@ -32,8 +32,8 @@ export async function enroll( config_id: configId, type, enrolled_at: enrolledAt, - user_provided_metadata: JSON.stringify(metadata?.userProvided ?? {}), - local_metadata: JSON.stringify(metadata?.local ?? {}), + user_provided_metadata: metadata?.userProvided ?? {}, + local_metadata: metadata?.local ?? {}, current_error_events: undefined, access_api_key_id: undefined, last_checkin: undefined, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts index b182662e0fb4e..11beba1cd7e43 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts @@ -19,8 +19,8 @@ export function savedObjectToAgent(so: SavedObject): Agent { current_error_events: so.attributes.current_error_events ? JSON.parse(so.attributes.current_error_events) : [], - local_metadata: JSON.parse(so.attributes.local_metadata), - user_provided_metadata: JSON.parse(so.attributes.user_provided_metadata), + local_metadata: so.attributes.local_metadata, + user_provided_metadata: so.attributes.user_provided_metadata, access_api_key: undefined, status: undefined, }; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts index b6de083cbe0cb..8140b1e6de470 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts @@ -18,8 +18,8 @@ describe('Agent status service', () => { type: AGENT_TYPE_PERMANENT, attributes: { active: false, - local_metadata: '{}', - user_provided_metadata: '{}', + local_metadata: {}, + user_provided_metadata: {}, }, } as SavedObject); const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); @@ -33,8 +33,8 @@ describe('Agent status service', () => { type: AGENT_TYPE_PERMANENT, attributes: { active: true, - local_metadata: '{}', - user_provided_metadata: '{}', + local_metadata: {}, + user_provided_metadata: {}, }, } as SavedObject); const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index a7019ebc0a271..27ed1de849987 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -8,6 +8,7 @@ import { ScopedClusterClient } from 'src/core/server'; export { // Object types Agent, + AgentMetadata, AgentSOAttributes, AgentStatus, AgentType, diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index 3fe4f828ba128..d22e3cd3fecdd 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -11,8 +11,8 @@ "shared_id": "agent1_filebeat", "config_id": "1", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -30,8 +30,8 @@ "active": true, "shared_id": "agent2_filebeat", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -49,8 +49,8 @@ "active": true, "shared_id": "agent3_metricbeat", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -68,8 +68,8 @@ "active": true, "shared_id": "agent4_metricbeat", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } diff --git a/x-pack/test/functional/es_archives/fleet/agents/mappings.json b/x-pack/test/functional/es_archives/fleet/agents/mappings.json index 5d5d373797d4c..409cc3c689eaf 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json +++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json @@ -227,7 +227,7 @@ "type": "date" }, "local_metadata": { - "type": "text" + "type": "flattened" }, "shared_id": { "type": "keyword" @@ -239,7 +239,7 @@ "type": "date" }, "user_provided_metadata": { - "type": "text" + "type": "flattened" }, "version": { "type": "keyword" From 793d6f5689c675b878a821226bcaf482ca4f8c98 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 30 Apr 2020 10:50:55 -0400 Subject: [PATCH 033/122] Always add Manager setup route. Fleet, if enabled (#64859) Co-authored-by: Elastic Machine --- x-pack/plugins/ingest_manager/server/plugin.ts | 2 +- .../plugins/ingest_manager/server/routes/setup/index.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 097825e0b69e1..3448685d1f279 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -150,6 +150,7 @@ export class IngestManagerPlugin const config = await this.config$.pipe(first()).toPromise(); // Register routes + registerSetupRoutes(router, config); registerAgentConfigRoutes(router); registerDatasourceRoutes(router); registerOutputRoutes(router); @@ -162,7 +163,6 @@ export class IngestManagerPlugin } if (config.fleet.enabled) { - registerSetupRoutes(router); registerAgentRoutes(router); registerEnrollmentApiKeyRoutes(router); registerInstallScriptRoutes({ diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts index edc9a0a268161..5ee7ee7733220 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts @@ -5,13 +5,14 @@ */ import { IRouter } from 'src/core/server'; import { PLUGIN_ID, FLEET_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; +import { IngestManagerConfigType } from '../../../common'; import { getFleetSetupHandler, createFleetSetupHandler, ingestManagerSetupHandler, } from './handlers'; -export const registerRoutes = (router: IRouter) => { +export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) => { // Ingest manager setup router.post( { @@ -23,6 +24,11 @@ export const registerRoutes = (router: IRouter) => { }, ingestManagerSetupHandler ); + + if (!config.fleet.enabled) { + return; + } + // Get Fleet setup router.get( { From 988b93edca7267bc20b902846591f71e2378d338 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 30 Apr 2020 10:52:10 -0400 Subject: [PATCH 034/122] [Lens] Hide some suggestions in preparation for pie charts (#64740) * [Lens] Hide some suggestions in preparation for pie charts * Suggest reordering the X and Break down by axis * More tweaks --- .../datatable_visualization/visualization.tsx | 4 +- .../indexpattern_suggestions.test.tsx | 56 +++++++++++++++ .../indexpattern_suggestions.ts | 2 +- x-pack/plugins/lens/public/types.ts | 9 ++- .../xy_visualization/xy_suggestions.test.ts | 72 ++++++++++++++++++- .../public/xy_visualization/xy_suggestions.ts | 26 +++++-- 6 files changed, 158 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 48729448b2ea5..21bbcce68bf36 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -126,8 +126,8 @@ export const datatableVisualization: Visualization< ], }, previewIcon: chartTableSVG, - // dont show suggestions for reduced versions or single-line tables - hide: table.changeType === 'reduced' || !table.isMultiRow, + // tables are hidden from suggestion bar, but used for drag & drop and chart switching + hide: true, }, ]; }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 02471b935c97c..f26fd39a60c0e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -1552,6 +1552,62 @@ describe('IndexPattern Data Source suggestions', () => { expect(suggestions[0].table.columns.length).toBe(1); expect(suggestions[0].table.columns[0].operation.label).toBe('Sum of field1'); }); + + it('contains a reordering suggestion when there are exactly 2 buckets', () => { + const initialState = testInitialState(); + const state: IndexPatternPrivateState = { + indexPatternRefs: [], + existingFields: {}, + currentIndexPatternId: '1', + indexPatterns: expectedIndexPatterns, + showEmptyFields: true, + layers: { + first: { + ...initialState.layers.first, + columns: { + id1: { + label: 'Date histogram', + dataType: 'date', + isBucketed: true, + + operationType: 'date_histogram', + sourceField: 'field2', + params: { + interval: 'd', + }, + }, + id2: { + label: 'Top 5', + dataType: 'string', + isBucketed: true, + + operationType: 'terms', + sourceField: 'field1', + params: { size: 5, orderBy: { type: 'alphabetical' }, orderDirection: 'asc' }, + }, + id3: { + label: 'Average of field1', + dataType: 'number', + isBucketed: false, + + operationType: 'avg', + sourceField: 'field1', + }, + }, + columnOrder: ['id1', 'id2', 'id3'], + }, + }, + }; + + const suggestions = getDatasourceSuggestionsFromCurrentState(state); + expect(suggestions).toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + changeType: 'reorder', + }), + }) + ); + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 44963722f8afc..487c1bf759fc2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -486,7 +486,7 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId layerId, updatedLayer, label: getNestedTitle([layer.columns[secondBucket], layer.columns[firstBucket]]), - changeType: 'extended', + changeType: 'reorder', }); } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index ed0af8545f012..04efc642793b0 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -103,9 +103,16 @@ export interface TableSuggestion { * * `unchanged` means the table is the same in the currently active configuration * * `reduced` means the table is a reduced version of the currently active table (some columns dropped, but not all of them) * * `extended` means the table is an extended version of the currently active table (added one or multiple additional columns) + * * `reorder` means the table columns have changed order, which change the data as well * * `layers` means the change is a change to the layer structure, not to the table */ -export type TableChangeType = 'initial' | 'unchanged' | 'reduced' | 'extended' | 'layers'; +export type TableChangeType = + | 'initial' + | 'unchanged' + | 'reduced' + | 'extended' + | 'reorder' + | 'layers'; export interface DatasourceSuggestion { state: T; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index ddbd9d11b5fad..73ff88e97f479 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -186,7 +186,7 @@ describe('xy_suggestions', () => { isMultiRow: true, columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], layerId: 'first', - changeType: 'unchanged', + changeType: 'extended', label: 'Datasource title', }, keptLayerIds: [], @@ -196,6 +196,34 @@ describe('xy_suggestions', () => { expect(suggestion.title).toEqual('Datasource title'); }); + test('suggests only stacked bar chart when xy chart is inactive', () => { + const [suggestion, ...rest] = getSuggestions({ + table: { + isMultiRow: true, + columns: [dateCol('date'), numCol('price')], + layerId: 'first', + changeType: 'unchanged', + label: 'Datasource title', + }, + keptLayerIds: [], + }); + + expect(rest).toHaveLength(0); + expect(suggestion.title).toEqual('Bar chart'); + expect(suggestion.state).toEqual( + expect.objectContaining({ + layers: [ + expect.objectContaining({ + seriesType: 'bar_stacked', + xAccessor: 'date', + accessors: ['price'], + splitAccessor: undefined, + }), + ], + }) + ); + }); + test('hides reduced suggestions if there is a current state', () => { const [suggestion, ...rest] = getSuggestions({ table: { @@ -224,7 +252,7 @@ describe('xy_suggestions', () => { expect(suggestion.hide).toBeTruthy(); }); - test('does not hide reduced suggestions if xy visualization is not active', () => { + test('hides reduced suggestions if xy visualization is not active', () => { const [suggestion, ...rest] = getSuggestions({ table: { isMultiRow: true, @@ -236,7 +264,7 @@ describe('xy_suggestions', () => { }); expect(rest).toHaveLength(0); - expect(suggestion.hide).toBeFalsy(); + expect(suggestion.hide).toBeTruthy(); }); test('only makes a seriesType suggestion for unchanged table without split', () => { @@ -419,6 +447,44 @@ describe('xy_suggestions', () => { }); }); + test('changes column mappings when suggestion is reorder', () => { + const currentState: XYState = { + legend: { isVisible: true, position: 'bottom' }, + preferredSeriesType: 'bar', + layers: [ + { + accessors: ['price'], + layerId: 'first', + seriesType: 'bar', + splitAccessor: 'category', + xAccessor: 'product', + }, + ], + }; + const [suggestion, ...rest] = getSuggestions({ + table: { + isMultiRow: true, + columns: [strCol('category'), strCol('product'), numCol('price')], + layerId: 'first', + changeType: 'reorder', + }, + state: currentState, + keptLayerIds: [], + }); + + expect(rest).toHaveLength(0); + expect(suggestion.state).toEqual({ + ...currentState, + layers: [ + { + ...currentState.layers[0], + xAccessor: 'category', + splitAccessor: 'product', + }, + ], + }); + }); + test('overwrites column to dimension mappings if a date dimension is added', () => { (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 5e9311bb1e928..abd7640344064 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -99,11 +99,14 @@ function getBucketMappings(table: TableSuggestion, currentState?: State) { // reverse the buckets before prioritization to always use the most inner // bucket of the highest-prioritized group as x value (don't use nested // buckets as split series) - const prioritizedBuckets = prioritizeColumns(buckets.reverse()); + const prioritizedBuckets = prioritizeColumns([...buckets].reverse()); if (!currentLayer || table.changeType === 'initial') { return prioritizedBuckets; } + if (table.changeType === 'reorder') { + return buckets; + } // if existing table is just modified, try to map buckets to the current dimensions const currentXColumnIndex = prioritizedBuckets.findIndex( @@ -175,12 +178,24 @@ function getSuggestionsForLayer({ keptLayerIds, }; - const isSameState = currentState && changeType === 'unchanged'; + // handles the simplest cases, acting as a chart switcher + if (!currentState && changeType === 'unchanged') { + return [ + { + ...buildSuggestion(options), + title: i18n.translate('xpack.lens.xySuggestions.barChartTitle', { + defaultMessage: 'Bar chart', + }), + }, + ]; + } + const isSameState = currentState && changeType === 'unchanged'; if (!isSameState) { return buildSuggestion(options); } + // Suggestions are either changing the data, or changing the way the data is used const sameStateSuggestions: Array> = []; // if current state is using the same data, suggest same chart with different presentational configuration @@ -374,8 +389,11 @@ function buildSuggestion({ return { title, score: getScore(yValues, splitBy, changeType), - // don't advertise chart of same type but with less data - hide: currentState && changeType === 'reduced', + hide: + // Only advertise very clear changes when XY chart is not active + (!currentState && changeType !== 'unchanged' && changeType !== 'extended') || + // Don't advertise removing dimensions + (currentState && changeType === 'reduced'), state, previewIcon: getIconForSeries(seriesType), }; From d3ba5b5a554ccde6ead6960102f6ec62e2670530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 30 Apr 2020 11:33:51 -0400 Subject: [PATCH 035/122] Update event log ILM policy defaults (#64675) * Initial work * Update docs * Add delete phase mention to docs Co-authored-by: Elastic Machine --- x-pack/plugins/event_log/README.md | 13 ++++++++++--- x-pack/plugins/event_log/server/es/documents.ts | 8 +++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index 38364033cb70b..941dedc3d1093 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -274,10 +274,16 @@ PUT _ilm/policy/event_log_policy "hot": { "actions": { "rollover": { - "max_size": "5GB", + "max_size": "50GB", "max_age": "30d" } } + }, + "delete": { + "min_age": "90d", + "actions": { + "delete": {} + } } } } @@ -285,10 +291,11 @@ PUT _ilm/policy/event_log_policy ``` This means that ILM would "rollover" the current index, say -`.kibana-event-log-000001` by creating a new index `.kibana-event-log-000002`, +`.kibana-event-log-8.0.0-000001` by creating a new index `.kibana-event-log-8.0.0-000002`, which would "inherit" everything from the index template, and then ILM will set the write index of the the alias to the new index. This would happen -when the original index grew past 5 GB, or was created more than 30 days ago. +when the original index grew past 50 GB, or was created more than 30 days ago. +After rollover, the indices will be removed after 90 days to avoid disks to fill up. For more relevant information on ILM, see: [getting started with ILM doc][] and [write index alias behavior][]: diff --git a/x-pack/plugins/event_log/server/es/documents.ts b/x-pack/plugins/event_log/server/es/documents.ts index a6af209d6d3a0..91b3db554964f 100644 --- a/x-pack/plugins/event_log/server/es/documents.ts +++ b/x-pack/plugins/event_log/server/es/documents.ts @@ -31,12 +31,18 @@ export function getIlmPolicy() { hot: { actions: { rollover: { - max_size: '5GB', + max_size: '50GB', max_age: '30d', // max_docs: 1, // you know, for testing }, }, }, + delete: { + min_age: '90d', + actions: { + delete: {}, + }, + }, }, }, }; From c131cb341b9fcc5ce340445c1562813182ff246a Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 30 Apr 2020 10:51:05 -0500 Subject: [PATCH 036/122] Refactor action messaging to report on No Data state (#64365) --- .../alerting/metrics/expression.tsx | 26 +++++ .../metrics/metric_threshold_alert_type.ts | 6 +- .../lib/alerting/metric_threshold/messages.ts | 109 ++++++++++++++++++ .../metric_threshold_executor.test.ts | 50 ++++++-- .../metric_threshold_executor.ts | 67 +++++++---- .../register_metric_threshold_alert_type.ts | 27 ++--- .../alerting/metric_threshold/test_mocks.ts | 8 ++ 7 files changed, 239 insertions(+), 54 deletions(-) create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_threshold/messages.ts diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index d4d53b81109c6..f2ac34ccb752f 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -13,6 +13,9 @@ import { EuiText, EuiFormRow, EuiButtonEmpty, + EuiCheckbox, + EuiToolTip, + EuiIcon, EuiFieldSearch, } from '@elastic/eui'; import { IFieldType } from 'src/plugins/data/public'; @@ -57,6 +60,7 @@ interface Props { groupBy?: string; filterQuery?: string; sourceId?: string; + alertOnNoData?: boolean; }; alertsContext: AlertsContextValue; setAlertParams(key: string, value: any): void; @@ -282,6 +286,28 @@ export const Expressions: React.FC = props => {
+ + + {i18n.translate('xpack.infra.metrics.alertFlyout.alertOnNoData', { + defaultMessage: "Alert me if there's no data", + })}{' '} + + + + + } + checked={alertParams.alertOnNoData} + onChange={e => setAlertParams('alertOnNoData', e.target.checked)} + /> + { + const gtText = i18n.translate('xpack.infra.metrics.alerting.threshold.gtComparator', { + defaultMessage: 'greater than', + }); + const ltText = i18n.translate('xpack.infra.metrics.alerting.threshold.ltComparator', { + defaultMessage: 'less than', + }); + const eqText = i18n.translate('xpack.infra.metrics.alerting.threshold.eqComparator', { + defaultMessage: 'equal to', + }); + + switch (comparator) { + case Comparator.BETWEEN: + return i18n.translate('xpack.infra.metrics.alerting.threshold.betweenComparator', { + defaultMessage: 'between', + }); + case Comparator.OUTSIDE_RANGE: + return i18n.translate('xpack.infra.metrics.alerting.threshold.outsideRangeComparator', { + defaultMessage: 'not between', + }); + case Comparator.GT: + return gtText; + case Comparator.LT: + return ltText; + case Comparator.GT_OR_EQ: + case Comparator.LT_OR_EQ: + if (threshold[0] === currentValue) return eqText; + else if (threshold[0] < currentValue) return ltText; + return gtText; + } +}; + +const thresholdToI18n = ([a, b]: number[]) => { + if (typeof b === 'undefined') return a; + return i18n.translate('xpack.infra.metrics.alerting.threshold.thresholdRange', { + defaultMessage: '{a} and {b}', + values: { a, b }, + }); +}; + +export const buildFiredAlertReason: (alertResult: { + metric: string; + comparator: Comparator; + threshold: number[]; + currentValue: number; +}) => string = ({ metric, comparator, threshold, currentValue }) => + i18n.translate('xpack.infra.metrics.alerting.threshold.firedAlertReason', { + defaultMessage: + '{metric} is {comparator} a threshold of {threshold} (current value is {currentValue})', + values: { + metric, + comparator: comparatorToI18n(comparator, threshold, currentValue), + threshold: thresholdToI18n(threshold), + currentValue, + }, + }); + +export const buildNoDataAlertReason: (alertResult: { + metric: string; + timeSize: number; + timeUnit: string; +}) => string = ({ metric, timeSize, timeUnit }) => + i18n.translate('xpack.infra.metrics.alerting.threshold.noDataAlertReason', { + defaultMessage: '{metric} has reported no data over the past {interval}', + values: { + metric, + interval: `${timeSize}${timeUnit}`, + }, + }); + +export const buildErrorAlertReason = (metric: string) => + i18n.translate('xpack.infra.metrics.alerting.threshold.errorAlertReason', { + defaultMessage: 'Elasticsearch failed when attempting to query data for {metric}', + values: { + metric, + }, + }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 24b6ba2ec378b..0007b8bd719f4 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -34,6 +34,8 @@ services.callCluster.mockImplementation(async (_: string, { body, index }: any) } if (metric === 'test.metric.2') { return mocks.alternateMetricResponse; + } else if (metric === 'test.metric.3') { + return mocks.emptyMetricResponse; } return mocks.basicMetricResponse; }); @@ -161,9 +163,9 @@ describe('The metric threshold alert type', () => { await execute(Comparator.GT, [0.75]); const { action } = mostRecentAction(instanceID); expect(action.group).toBe('*'); - expect(action.valueOf.condition0).toBe(1); - expect(action.thresholdOf.condition0).toStrictEqual([0.75]); - expect(action.metricOf.condition0).toBe('test.metric.1'); + expect(action.reason).toContain('current value is 1'); + expect(action.reason).toContain('threshold of 0.75'); + expect(action.reason).toContain('test.metric.1'); }); test('fetches the index pattern dynamically', async () => { await execute(Comparator.LT, [17], 'alternate'); @@ -271,12 +273,14 @@ describe('The metric threshold alert type', () => { const instanceID = 'test-*'; await execute(Comparator.GT_OR_EQ, [1.0], [3.0]); const { action } = mostRecentAction(instanceID); - expect(action.valueOf.condition0).toBe(1); - expect(action.valueOf.condition1).toBe(3.5); - expect(action.thresholdOf.condition0).toStrictEqual([1.0]); - expect(action.thresholdOf.condition1).toStrictEqual([3.0]); - expect(action.metricOf.condition0).toBe('test.metric.1'); - expect(action.metricOf.condition1).toBe('test.metric.2'); + const reasons = action.reason.split('\n'); + expect(reasons.length).toBe(2); + expect(reasons[0]).toContain('test.metric.1'); + expect(reasons[1]).toContain('test.metric.2'); + expect(reasons[0]).toContain('current value is 1'); + expect(reasons[1]).toContain('current value is 3.5'); + expect(reasons[0]).toContain('threshold of 1'); + expect(reasons[1]).toContain('threshold of 3'); }); }); describe('querying with the count aggregator', () => { @@ -305,4 +309,32 @@ describe('The metric threshold alert type', () => { expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); + describe("querying a metric that hasn't reported data", () => { + const instanceID = 'test-*'; + const execute = (alertOnNoData: boolean) => + executor({ + services, + params: { + criteria: [ + { + ...baseCriterion, + comparator: Comparator.GT, + threshold: 1, + metric: 'test.metric.3', + }, + ], + alertOnNoData, + }, + }); + test('sends a No Data alert when configured to do so', async () => { + await execute(true); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.NO_DATA); + }); + test('does not send a No Data alert when not configured to do so', async () => { + await execute(false); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.NO_DATA); + }); + }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index cf691f73bdc2c..bd77e5e2daf42 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -12,6 +12,13 @@ import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler'; import { getAllCompositeData } from '../../../utils/get_all_composite_data'; import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; import { MetricExpressionParams, Comparator, Aggregators, AlertStates } from './types'; +import { + buildErrorAlertReason, + buildFiredAlertReason, + buildNoDataAlertReason, + DOCUMENT_COUNT_I18N, + stateToAlertMessage, +} from './messages'; import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getDateHistogramOffset } from '../../snapshot/query_helpers'; @@ -258,24 +265,14 @@ const comparatorMap = { [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b, }; -const mapToConditionsLookup = ( - list: any[], - mapFn: (value: any, index: number, array: any[]) => unknown -) => - list - .map(mapFn) - .reduce( - (result: Record, value, i) => ({ ...result, [`condition${i}`]: value }), - {} - ); - export const createMetricThresholdExecutor = (alertUUID: string) => async function({ services, params }: AlertExecutorOptions) { - const { criteria, groupBy, filterQuery, sourceId } = params as { + const { criteria, groupBy, filterQuery, sourceId, alertOnNoData } = params as { criteria: MetricExpressionParams[]; groupBy: string | undefined; filterQuery: string | undefined; sourceId?: string; + alertOnNoData: boolean; }; const alertResults = await Promise.all( @@ -286,9 +283,11 @@ export const createMetricThresholdExecutor = (alertUUID: string) => const { threshold, comparator } = criterion; const comparisonFunction = comparatorMap[comparator]; return mapValues(currentValues, value => ({ + ...criterion, + metric: criterion.metric ?? DOCUMENT_COUNT_I18N, + currentValue: value, shouldFire: value !== undefined && value !== null && comparisonFunction(value, threshold), - currentValue: value, isNoData: value === null, isError: value === undefined, })); @@ -306,23 +305,43 @@ export const createMetricThresholdExecutor = (alertUUID: string) => // whole alert is in a No Data/Error state const isNoData = alertResults.some(result => result[group].isNoData); const isError = alertResults.some(result => result[group].isError); - if (shouldAlertFire) { + + const nextState = isError + ? AlertStates.ERROR + : isNoData + ? AlertStates.NO_DATA + : shouldAlertFire + ? AlertStates.ALERT + : AlertStates.OK; + + let reason; + if (nextState === AlertStates.ALERT) { + reason = alertResults.map(result => buildFiredAlertReason(result[group])).join('\n'); + } + if (alertOnNoData) { + if (nextState === AlertStates.NO_DATA) { + reason = alertResults + .filter(result => result[group].isNoData) + .map(result => buildNoDataAlertReason(result[group])) + .join('\n'); + } else if (nextState === AlertStates.ERROR) { + reason = alertResults + .filter(result => result[group].isError) + .map(result => buildErrorAlertReason(result[group].metric)) + .join('\n'); + } + } + if (reason) { alertInstance.scheduleActions(FIRED_ACTIONS.id, { group, - valueOf: mapToConditionsLookup(alertResults, result => result[group].currentValue), - thresholdOf: mapToConditionsLookup(criteria, criterion => criterion.threshold), - metricOf: mapToConditionsLookup(criteria, criterion => criterion.metric), + alertState: stateToAlertMessage[nextState], + reason, }); } + // Future use: ability to fetch display current alert state alertInstance.replaceState({ - alertState: isError - ? AlertStates.ERROR - : isNoData - ? AlertStates.NO_DATA - : shouldAlertFire - ? AlertStates.ALERT - : AlertStates.OK, + alertState: nextState, }); } }; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 3415ae9873bfb..029491c1168cf 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -55,27 +55,18 @@ export async function registerMetricThresholdAlertType( } ); - const valueOfActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.valueOfActionVariableDescription', + const alertStateActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.alertStateActionVariableDescription', { - defaultMessage: - 'Record of the current value of the watched metric; grouped by condition, i.e valueOf.condition0, valueOf.condition1, etc.', - } - ); - - const thresholdOfActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.thresholdOfActionVariableDescription', - { - defaultMessage: - 'Record of the alerting threshold; grouped by condition, i.e thresholdOf.condition0, thresholdOf.condition1, etc.', + defaultMessage: 'Current state of the alert', } ); - const metricOfActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.metricOfActionVariableDescription', + const reasonActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.reasonActionVariableDescription', { defaultMessage: - 'Record of the watched metric; grouped by condition, i.e metricOf.condition0, metricOf.condition1, etc.', + 'A description of why the alert is in this state, including which metrics have crossed which thresholds', } ); @@ -88,6 +79,7 @@ export async function registerMetricThresholdAlertType( groupBy: schema.maybe(schema.string()), filterQuery: schema.maybe(schema.string()), sourceId: schema.string(), + alertOnNoData: schema.maybe(schema.boolean()), }), }, defaultActionGroupId: FIRED_ACTIONS.id, @@ -96,9 +88,8 @@ export async function registerMetricThresholdAlertType( actionVariables: { context: [ { name: 'group', description: groupActionVariableDescription }, - { name: 'valueOf', description: valueOfActionVariableDescription }, - { name: 'thresholdOf', description: thresholdOfActionVariableDescription }, - { name: 'metricOf', description: metricOfActionVariableDescription }, + { name: 'alertState', description: alertStateActionVariableDescription }, + { name: 'reason', description: reasonActionVariableDescription }, ], }, }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index 66e0a363c8983..fa55f80e472de 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -53,6 +53,14 @@ export const alternateMetricResponse = { }, }; +export const emptyMetricResponse = { + aggregations: { + aggregatedIntervals: { + buckets: [], + }, + }, +}; + export const basicCompositeResponse = { aggregations: { groupings: { From 671d750c03068696abae097c83ffbc05d03cba3a Mon Sep 17 00:00:00 2001 From: The SpaceCake Project Date: Thu, 30 Apr 2020 12:28:55 -0400 Subject: [PATCH 037/122] lookback adjust (#64837) increase lookback to 15 minutes for latency in endpoint signal rules --- ...elastic_endpoint_security_adversary_behavior_detected.json | 2 +- .../elastic_endpoint_security_cred_dumping_detected.json | 4 ++-- .../elastic_endpoint_security_cred_dumping_prevented.json | 2 +- .../elastic_endpoint_security_cred_manipulation_detected.json | 2 +- ...elastic_endpoint_security_cred_manipulation_prevented.json | 2 +- .../elastic_endpoint_security_exploit_detected.json | 2 +- .../elastic_endpoint_security_exploit_prevented.json | 2 +- .../elastic_endpoint_security_malware_detected.json | 2 +- .../elastic_endpoint_security_malware_prevented.json | 2 +- .../elastic_endpoint_security_permission_theft_detected.json | 2 +- .../elastic_endpoint_security_permission_theft_prevented.json | 2 +- .../elastic_endpoint_security_process_injection_detected.json | 2 +- ...elastic_endpoint_security_process_injection_prevented.json | 2 +- .../elastic_endpoint_security_ransomware_detected.json | 2 +- .../elastic_endpoint_security_ransomware_prevented.json | 2 +- 15 files changed, 16 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json index cfc322788d4be..c83c0e01d7fa0 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json index 0647fe9c9ce10..18472abbd70d7 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], @@ -17,4 +17,4 @@ ], "type": "query", "version": 2 -} \ No newline at end of file +} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json index 036c88688d9bd..03024ad15396e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json index 0fe610d551152..e5a128029f585 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Credential Manipulation. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json index a317c77bcd90a..1c05743fae62f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Credential Manipulation. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json index 97640c0cea9b2..3396a8563ba1c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected an Exploit. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json index 069687a5af00f..2f70c539414c6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented an Exploit. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json index a7d3371190ced..cbf6c286a439f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Malware. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json index dd7bf72c34f90..49c7c160e5daf 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Malware. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json index a8e102cc4619d..e836bd037ddc5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Permission Theft. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json index c97330f2349eb..e9ac8d7ba6686 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Permission Theft. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json index e644c0e8d66eb..8e25832b0e89a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Process Injection. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json index 61cbe267f9a46..a59428275ca22 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Process Injection. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json index 0e88b26cb2c75..22091d8c9b68f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Ransomware. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json index ba341f059f26d..947bfcbba39a0 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Ransomware. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], From 9112b6c1f1ef6def9b1dc83f18276d9cbd24046b Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 30 Apr 2020 10:32:26 -0600 Subject: [PATCH 038/122] Avoid race condition between HttpServer.stop() and HttpServerSetup methods (#64487) --- src/core/server/http/http_server.test.ts | 58 ++++++++++++++++++++++++ src/core/server/http/http_server.ts | 28 +++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 27db79bb94d25..4fb433b5c77ba 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -1068,6 +1068,14 @@ describe('setup contract', () => { await create(); expect(create()).rejects.toThrowError('A cookieSessionStorageFactory was already created'); }); + + it('does not throw if called after stop', async () => { + const { createCookieSessionStorageFactory } = await server.setup(config); + await server.stop(); + expect(() => { + createCookieSessionStorageFactory(cookieOptions); + }).not.toThrow(); + }); }); describe('#isTlsEnabled', () => { @@ -1113,4 +1121,54 @@ describe('setup contract', () => { expect(getServerInfo().protocol).toEqual('https'); }); }); + + describe('#registerStaticDir', () => { + it('does not throw if called after stop', async () => { + const { registerStaticDir } = await server.setup(config); + await server.stop(); + expect(() => { + registerStaticDir('/path1/{path*}', '/path/to/resource'); + }).not.toThrow(); + }); + }); + + describe('#registerOnPreAuth', () => { + test('does not throw if called after stop', async () => { + const { registerOnPreAuth } = await server.setup(config); + await server.stop(); + expect(() => { + registerOnPreAuth((req, res) => res.unauthorized()); + }).not.toThrow(); + }); + }); + + describe('#registerOnPostAuth', () => { + test('does not throw if called after stop', async () => { + const { registerOnPostAuth } = await server.setup(config); + await server.stop(); + expect(() => { + registerOnPostAuth((req, res) => res.unauthorized()); + }).not.toThrow(); + }); + }); + + describe('#registerOnPreResponse', () => { + test('does not throw if called after stop', async () => { + const { registerOnPreResponse } = await server.setup(config); + await server.stop(); + expect(() => { + registerOnPreResponse((req, res, t) => t.next()); + }).not.toThrow(); + }); + }); + + describe('#registerAuth', () => { + test('does not throw if called after stop', async () => { + const { registerAuth } = await server.setup(config); + await server.stop(); + expect(() => { + registerAuth((req, res) => res.unauthorized()); + }).not.toThrow(); + }); + }); }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 77d3d99fb48cb..92ac5220735a1 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -74,6 +74,7 @@ export class HttpServer { private registeredRouters = new Set(); private authRegistered = false; private cookieSessionStorageCreated = false; + private stopped = false; private readonly log: Logger; private readonly authState: AuthStateStorage; @@ -144,6 +145,10 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Http server is not setup up yet'); } + if (this.stopped) { + this.log.warn(`start called after stop`); + return; + } this.log.debug('starting http server'); for (const router of this.registeredRouters) { @@ -189,13 +194,13 @@ export class HttpServer { } public async stop() { + this.stopped = true; if (this.server === undefined) { return; } this.log.debug('stopping http server'); await this.server.stop(); - this.server = undefined; } private getAuthOption( @@ -234,6 +239,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`setupConditionalCompression called after stop`); + } const { enabled, referrerWhitelist: list } = config.compression; if (!enabled) { @@ -261,6 +269,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`registerOnPostAuth called after stop`); + } this.server.ext('onPostAuth', adoptToHapiOnPostAuthFormat(fn, this.log)); } @@ -269,6 +280,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`registerOnPreAuth called after stop`); + } this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn, this.log)); } @@ -277,6 +291,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`registerOnPreResponse called after stop`); + } this.server.ext('onPreResponse', adoptToHapiOnPreResponseFormat(fn, this.log)); } @@ -288,6 +305,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`createCookieSessionStorageFactory called after stop`); + } if (this.cookieSessionStorageCreated) { throw new Error('A cookieSessionStorageFactory was already created'); } @@ -305,6 +325,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`registerAuth called after stop`); + } if (this.authRegistered) { throw new Error('Auth interceptor was already registered'); } @@ -348,6 +371,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Http server is not setup up yet'); } + if (this.stopped) { + this.log.warn(`registerStaticDir called after stop`); + } this.server.route({ path, From a7291fa8c82f06c6d2f488c3d5694f71893ff52e Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 30 Apr 2020 13:03:08 -0400 Subject: [PATCH 039/122] [EPM] Adding support for nested fields (#64829) * Allowing nested types to be merged with group * Adding nested to case and handling other fields * Cleaing up if logic * Removing unneeded if statement * Adding nested type to switch and more tests * Keeping functions immutable --- .../elasticsearch/template/template.test.ts | 106 ++++++++++-- .../epm/elasticsearch/template/template.ts | 43 ++++- .../server/services/epm/fields/field.test.ts | 162 ++++++++++++++++++ .../server/services/epm/fields/field.ts | 54 +++++- 4 files changed, 339 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts index 25180244b0214..cacf84381dd88 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts @@ -93,7 +93,7 @@ test('tests processing text field with multi fields', () => { const fields: Field[] = safeLoad(textWithMultiFieldsLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(textWithMultiFieldsMapping)); + expect(mappings).toEqual(textWithMultiFieldsMapping); }); test('tests processing keyword field with multi fields', () => { @@ -127,7 +127,7 @@ test('tests processing keyword field with multi fields', () => { const fields: Field[] = safeLoad(keywordWithMultiFieldsLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(keywordWithMultiFieldsMapping)); + expect(mappings).toEqual(keywordWithMultiFieldsMapping); }); test('tests processing keyword field with multi fields with analyzed text field', () => { @@ -159,7 +159,7 @@ test('tests processing keyword field with multi fields with analyzed text field' const fields: Field[] = safeLoad(keywordWithAnalyzedMultiFieldsLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(keywordWithAnalyzedMultiFieldsMapping)); + expect(mappings).toEqual(keywordWithAnalyzedMultiFieldsMapping); }); test('tests processing object field with no other attributes', () => { @@ -177,7 +177,7 @@ test('tests processing object field with no other attributes', () => { const fields: Field[] = safeLoad(objectFieldLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldMapping)); + expect(mappings).toEqual(objectFieldMapping); }); test('tests processing object field with enabled set to false', () => { @@ -197,7 +197,7 @@ test('tests processing object field with enabled set to false', () => { const fields: Field[] = safeLoad(objectFieldEnabledFalseLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldEnabledFalseMapping)); + expect(mappings).toEqual(objectFieldEnabledFalseMapping); }); test('tests processing object field with dynamic set to false', () => { @@ -217,7 +217,7 @@ test('tests processing object field with dynamic set to false', () => { const fields: Field[] = safeLoad(objectFieldDynamicFalseLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicFalseMapping)); + expect(mappings).toEqual(objectFieldDynamicFalseMapping); }); test('tests processing object field with dynamic set to true', () => { @@ -237,7 +237,7 @@ test('tests processing object field with dynamic set to true', () => { const fields: Field[] = safeLoad(objectFieldDynamicTrueLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicTrueMapping)); + expect(mappings).toEqual(objectFieldDynamicTrueMapping); }); test('tests processing object field with dynamic set to strict', () => { @@ -257,7 +257,7 @@ test('tests processing object field with dynamic set to strict', () => { const fields: Field[] = safeLoad(objectFieldDynamicStrictLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicStrictMapping)); + expect(mappings).toEqual(objectFieldDynamicStrictMapping); }); test('tests processing object field with property', () => { @@ -282,7 +282,7 @@ test('tests processing object field with property', () => { const fields: Field[] = safeLoad(objectFieldWithPropertyLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldWithPropertyMapping)); + expect(mappings).toEqual(objectFieldWithPropertyMapping); }); test('tests processing object field with property, reverse order', () => { @@ -291,10 +291,12 @@ test('tests processing object field with property, reverse order', () => { type: keyword - name: a type: object + dynamic: false `; const objectFieldWithPropertyReversedMapping = { properties: { a: { + dynamic: false, properties: { b: { ignore_above: 1024, @@ -307,7 +309,91 @@ test('tests processing object field with property, reverse order', () => { const fields: Field[] = safeLoad(objectFieldWithPropertyReversedLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldWithPropertyReversedMapping)); + expect(mappings).toEqual(objectFieldWithPropertyReversedMapping); +}); + +test('tests processing nested field with property', () => { + const nestedYaml = ` + - name: a.b + type: keyword + - name: a + type: nested + dynamic: false + `; + const expectedMapping = { + properties: { + a: { + dynamic: false, + type: 'nested', + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(nestedYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(expectedMapping); +}); + +test('tests processing nested field with property, nested field first', () => { + const nestedYaml = ` + - name: a + type: nested + include_in_parent: true + - name: a.b + type: keyword + `; + const expectedMapping = { + properties: { + a: { + include_in_parent: true, + type: 'nested', + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(nestedYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(expectedMapping); +}); + +test('tests processing nested leaf field with properties', () => { + const nestedYaml = ` + - name: a + type: object + dynamic: false + - name: a.b + type: nested + enabled: false + `; + const expectedMapping = { + properties: { + a: { + dynamic: false, + properties: { + b: { + enabled: false, + type: 'nested', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(nestedYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(expectedMapping); }); test('tests constant_keyword field type handling', () => { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index 9736f6d1cbd3c..c45c7e706be58 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -71,7 +71,14 @@ export function generateMappings(fields: Field[]): IndexTemplateMappings { switch (type) { case 'group': - fieldProps = generateMappings(field.fields!); + fieldProps = { ...generateMappings(field.fields!), ...generateDynamicAndEnabled(field) }; + break; + case 'group-nested': + fieldProps = { + ...generateMappings(field.fields!), + ...generateNestedProps(field), + type: 'nested', + }; break; case 'integer': fieldProps.type = 'long'; @@ -95,13 +102,10 @@ export function generateMappings(fields: Field[]): IndexTemplateMappings { } break; case 'object': - fieldProps.type = 'object'; - if (field.hasOwnProperty('enabled')) { - fieldProps.enabled = field.enabled; - } - if (field.hasOwnProperty('dynamic')) { - fieldProps.dynamic = field.dynamic; - } + fieldProps = { ...fieldProps, ...generateDynamicAndEnabled(field), type: 'object' }; + break; + case 'nested': + fieldProps = { ...fieldProps, ...generateNestedProps(field), type: 'nested' }; break; case 'array': // this assumes array fields were validated in an earlier step @@ -128,6 +132,29 @@ export function generateMappings(fields: Field[]): IndexTemplateMappings { return { properties: props }; } +function generateDynamicAndEnabled(field: Field) { + const props: Properties = {}; + if (field.hasOwnProperty('enabled')) { + props.enabled = field.enabled; + } + if (field.hasOwnProperty('dynamic')) { + props.dynamic = field.dynamic; + } + return props; +} + +function generateNestedProps(field: Field) { + const props = generateDynamicAndEnabled(field); + + if (field.hasOwnProperty('include_in_parent')) { + props.include_in_parent = field.include_in_parent; + } + if (field.hasOwnProperty('include_in_root')) { + props.include_in_root = field.include_in_root; + } + return props; +} + function generateMultiFields(fields: Fields): MultiFields { const multiFields: MultiFields = {}; if (fields) { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts index 42989bb1e3ac9..f0ff4c6125452 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts @@ -210,4 +210,166 @@ describe('processFields', () => { JSON.stringify(objectFieldWithPropertyExpanded) ); }); + + test('correctly handles properties of object type fields where object comes second', () => { + const nested = [ + { + name: 'a.b', + type: 'keyword', + }, + { + name: 'a', + type: 'object', + dynamic: true, + }, + ]; + + const nestedExpanded = [ + { + name: 'a', + type: 'group', + dynamic: true, + fields: [ + { + name: 'b', + type: 'keyword', + }, + ], + }, + ]; + expect(processFields(nested)).toEqual(nestedExpanded); + }); + + test('correctly handles properties of nested type fields', () => { + const nested = [ + { + name: 'a', + type: 'nested', + dynamic: true, + }, + { + name: 'a.b', + type: 'keyword', + }, + ]; + + const nestedExpanded = [ + { + name: 'a', + type: 'group-nested', + dynamic: true, + fields: [ + { + name: 'b', + type: 'keyword', + }, + ], + }, + ]; + expect(processFields(nested)).toEqual(nestedExpanded); + }); + + test('correctly handles properties of nested type where nested top level comes second', () => { + const nested = [ + { + name: 'a.b', + type: 'keyword', + }, + { + name: 'a', + type: 'nested', + dynamic: true, + }, + ]; + + const nestedExpanded = [ + { + name: 'a', + type: 'group-nested', + dynamic: true, + fields: [ + { + name: 'b', + type: 'keyword', + }, + ], + }, + ]; + expect(processFields(nested)).toEqual(nestedExpanded); + }); + + test('ignores redefinitions of an object field', () => { + const object = [ + { + name: 'a', + type: 'object', + dynamic: true, + }, + { + name: 'a', + type: 'object', + dynamic: false, + }, + ]; + + const objectExpected = [ + { + name: 'a', + type: 'object', + // should preserve the field that was parsed first which had dynamic: true + dynamic: true, + }, + ]; + expect(processFields(object)).toEqual(objectExpected); + }); + + test('ignores redefinitions of a nested field', () => { + const nested = [ + { + name: 'a', + type: 'nested', + dynamic: true, + }, + { + name: 'a', + type: 'nested', + dynamic: false, + }, + ]; + + const nestedExpected = [ + { + name: 'a', + type: 'nested', + // should preserve the field that was parsed first which had dynamic: true + dynamic: true, + }, + ]; + expect(processFields(nested)).toEqual(nestedExpected); + }); + + test('ignores redefinitions of a nested and object field', () => { + const nested = [ + { + name: 'a', + type: 'nested', + dynamic: true, + }, + { + name: 'a', + type: 'object', + dynamic: false, + }, + ]; + + const nestedExpected = [ + { + name: 'a', + type: 'nested', + // should preserve the field that was parsed first which had dynamic: true + dynamic: true, + }, + ]; + expect(processFields(nested)).toEqual(nestedExpected); + }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts index edf7624d3f0d5..abaf7ab5b0dfc 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts @@ -28,6 +28,8 @@ export interface Field { object_type?: string; scaling_factor?: number; dynamic?: 'strict' | boolean; + include_in_parent?: boolean; + include_in_root?: boolean; // Kibana specific analyzed?: boolean; @@ -108,18 +110,54 @@ function dedupFields(fields: Fields): Fields { return f.name === field.name; }); if (found) { + // remove name, type, and fields from `field` variable so we avoid merging them into `found` + const { name, type, fields: nestedFields, ...importantFieldProps } = field; + /** + * There are a couple scenarios this if is trying to account for: + * Example 1 + * - name: a.b + * - name: a + * In this scenario found will be `group` and field could be either `object` or `nested` + * Example 2 + * - name: a + * - name: a.b + * In this scenario found could be `object` or `nested` and field will be group + */ if ( - (found.type === 'group' || found.type === 'object') && - field.type === 'group' && - field.fields + // only merge if found is a group and field is object, nested, or group. + // Or if found is object, or nested, and field is a group. + // This is to avoid merging two objects, or nested, or object with a nested. + (found.type === 'group' && + (field.type === 'object' || field.type === 'nested' || field.type === 'group')) || + ((found.type === 'object' || found.type === 'nested') && field.type === 'group') ) { - if (!found.fields) { - found.fields = []; + // if the new field has properties let's dedup and concat them with the already existing found variable in + // the array + if (field.fields) { + // if the found type was object or nested it won't have a fields array so let's initialize it + if (!found.fields) { + found.fields = []; + } + found.fields = dedupFields(found.fields.concat(field.fields)); } - found.type = 'group'; - found.fields = dedupFields(found.fields.concat(field.fields)); + + // if found already had fields or got new ones from the new field coming in we need to assign the right + // type to it + if (found.fields) { + // If this field is supposed to be `nested` and we have fields, we need to preserve the fact that it is + // supposed to be `nested` for when the template is actually generated + if (found.type === 'nested' || field.type === 'nested') { + found.type = 'group-nested'; + } else { + // found was either `group` already or `object` so just set it to `group` + found.type = 'group'; + } + } + // we need to merge in other properties (like `dynamic`) that might exist + Object.assign(found, importantFieldProps); + // if `field.type` wasn't group object or nested, then there's a conflict in types, so lets ignore it } else { - // only 'group' fields can be merged in this way + // only `group`, `object`, and `nested` fields can be merged in this way // XXX: don't abort on error for now // see discussion in https://github.com/elastic/kibana/pull/59894 // throw new Error( From 693a84aace273108d2ebf51b3a8d018cd97ed41c Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 30 Apr 2020 19:16:31 +0200 Subject: [PATCH 040/122] load SettingsOptions component lazily (#64638) Co-authored-by: Elastic Machine --- .../vis_type_markdown/public/markdown_vis.ts | 2 +- .../public/settings_options.tsx | 4 ++- .../public/settings_options_lazy.tsx | 30 +++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 src/plugins/vis_type_markdown/public/settings_options_lazy.tsx diff --git a/src/plugins/vis_type_markdown/public/markdown_vis.ts b/src/plugins/vis_type_markdown/public/markdown_vis.ts index b84d9638eb973..3309330d7527c 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/plugins/vis_type_markdown/public/markdown_vis.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { MarkdownVisWrapper } from './markdown_vis_controller'; import { MarkdownOptions } from './markdown_options'; -import { SettingsOptions } from './settings_options'; +import { SettingsOptions } from './settings_options_lazy'; import { DefaultEditorSize } from '../../vis_default_editor/public'; export const markdownVisDefinition = { diff --git a/src/plugins/vis_type_markdown/public/settings_options.tsx b/src/plugins/vis_type_markdown/public/settings_options.tsx index 6f6a80564ce07..bf4570db5d4a0 100644 --- a/src/plugins/vis_type_markdown/public/settings_options.tsx +++ b/src/plugins/vis_type_markdown/public/settings_options.tsx @@ -52,4 +52,6 @@ function SettingsOptions({ stateParams, setValue }: VisOptionsProps import('./settings_options')); + +export const SettingsOptions = (props: any) => ( + }> + + +); From 3442c25b9f7bf0dd5a8b6ad614f5e0e64b5d318a Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 30 Apr 2020 19:18:07 +0200 Subject: [PATCH 041/122] lazy load React components + VegaParser (#64749) --- .../vis_type_vega/public/vega_request_handler.ts | 13 +++++++++---- .../vis_type_vega/public/vega_visualization.js | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts index 196e8fdcbafda..efc02e368efa8 100644 --- a/src/plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -19,8 +19,6 @@ import { Filter, esQuery, TimeRange, Query } from '../../data/public'; -// @ts-ignore -import { VegaParser } from './data_model/vega_parser'; // @ts-ignore import { SearchCache } from './data_model/search_cache'; // @ts-ignore @@ -46,7 +44,12 @@ export function createVegaRequestHandler({ const { timefilter } = data.query.timefilter; const timeCache = new TimeCache(timefilter, 3 * 1000); - return ({ timeRange, filters, query, visParams }: VegaRequestHandlerParams) => { + return async function vegaRequestHandler({ + timeRange, + filters, + query, + visParams, + }: VegaRequestHandlerParams) { if (!searchCache) { searchCache = new SearchCache(getData().search.__LEGACY.esClient, { max: 10, @@ -58,8 +61,10 @@ export function createVegaRequestHandler({ const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); const filtersDsl = esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs); + // @ts-ignore + const { VegaParser } = await import('./data_model/vega_parser'); const vp = new VegaParser(visParams.spec, searchCache, timeCache, filtersDsl, serviceSettings); - return vp.parseAsync(); + return await vp.parseAsync(); }; } diff --git a/src/plugins/vis_type_vega/public/vega_visualization.js b/src/plugins/vis_type_vega/public/vega_visualization.js index a6e911de7f0cb..1fcb89f04457d 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.js @@ -17,8 +17,6 @@ * under the License. */ import { i18n } from '@kbn/i18n'; -import { VegaView } from './vega_view/vega_view'; -import { VegaMapView } from './vega_view/vega_map_view'; import { getNotifications, getData, getSavedObjects } from './services'; export const createVegaVisualization = ({ serviceSettings }) => @@ -117,8 +115,10 @@ export const createVegaVisualization = ({ serviceSettings }) => if (vegaParser.useMap) { const services = { toastService: getNotifications().toasts }; + const { VegaMapView } = await import('./vega_view/vega_map_view'); this._vegaView = new VegaMapView(vegaViewParams, services); } else { + const { VegaView } = await import('./vega_view/vega_view'); this._vegaView = new VegaView(vegaViewParams); } await this._vegaView.init(); From 0efd02b49dbbc825f8694237dc74bfa7685682d3 Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Thu, 30 Apr 2020 17:30:06 +0000 Subject: [PATCH 042/122] change createIndexPattern to do what it takes to make the indexPatternName* (#64646) --- .../apps/discover/_discover_histogram.js | 2 +- .../apps/getting_started/_shakespeare.js | 6 ++--- .../_index_pattern_create_delete.js | 5 +--- test/functional/page_objects/settings_page.ts | 26 ++++++++++++++++--- x-pack/test/functional/apps/graph/graph.ts | 4 +-- 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/test/functional/apps/discover/_discover_histogram.js b/test/functional/apps/discover/_discover_histogram.js index eeef3333aab0f..dcd185eba00e6 100644 --- a/test/functional/apps/discover/_discover_histogram.js +++ b/test/functional/apps/discover/_discover_histogram.js @@ -48,7 +48,7 @@ export default function({ getService, getPageObjects }) { log.debug('create long_window_logstash index pattern'); // NOTE: long_window_logstash load does NOT create index pattern - await PageObjects.settings.createIndexPattern('long-window-logstash-'); + await PageObjects.settings.createIndexPattern('long-window-logstash-*'); await kibanaServer.uiSettings.replace(defaultSettings); await browser.refresh(); diff --git a/test/functional/apps/getting_started/_shakespeare.js b/test/functional/apps/getting_started/_shakespeare.js index 9a4bb0081b7ad..3a3d6b93e166b 100644 --- a/test/functional/apps/getting_started/_shakespeare.js +++ b/test/functional/apps/getting_started/_shakespeare.js @@ -59,9 +59,9 @@ export default function({ getService, getPageObjects }) { it('should create shakespeare index pattern', async function() { log.debug('Create shakespeare index pattern'); - await PageObjects.settings.createIndexPattern('shakes', null); + await PageObjects.settings.createIndexPattern('shakespeare', null); const patternName = await PageObjects.settings.getIndexPageHeading(); - expect(patternName).to.be('shakes*'); + expect(patternName).to.be('shakespeare'); }); // https://www.elastic.co/guide/en/kibana/current/tutorial-visualizing.html @@ -74,7 +74,7 @@ export default function({ getService, getPageObjects }) { log.debug('create shakespeare vertical bar chart'); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVerticalBarChart(); - await PageObjects.visualize.clickNewSearch('shakes*'); + await PageObjects.visualize.clickNewSearch('shakespeare'); await PageObjects.visChart.waitForVisualization(); const expectedChartValues = [111396]; diff --git a/test/functional/apps/management/_index_pattern_create_delete.js b/test/functional/apps/management/_index_pattern_create_delete.js index 616e2297b2f51..2545b8f324d1b 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.js +++ b/test/functional/apps/management/_index_pattern_create_delete.js @@ -44,10 +44,7 @@ export default function({ getService, getPageObjects }) { it('should handle special charaters in template input', async () => { await PageObjects.settings.clickAddNewIndexPatternButton(); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.settings.setIndexPatternField({ - indexPatternName: '❤️', - expectWildcard: false, - }); + await PageObjects.settings.setIndexPatternField('❤️'); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async () => { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 8864eda3823ef..81d22838d1e8b 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -334,7 +334,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider } await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async () => { - await this.setIndexPatternField({ indexPatternName }); + await this.setIndexPatternField(indexPatternName); }); await PageObjects.common.sleep(2000); await (await this.getCreateIndexPatternGoToStep2Button()).click(); @@ -375,14 +375,32 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider return indexPatternId; } - async setIndexPatternField({ indexPatternName = 'logstash-', expectWildcard = true } = {}) { + async setIndexPatternField(indexPatternName = 'logstash-*') { log.debug(`setIndexPatternField(${indexPatternName})`); const field = await this.getIndexPatternField(); await field.clearValue(); - await field.type(indexPatternName, { charByChar: true }); + if ( + indexPatternName.charAt(0) === '*' && + indexPatternName.charAt(indexPatternName.length - 1) === '*' + ) { + // this is a special case when the index pattern name starts with '*' + // like '*:makelogs-*' where the UI will not append * + await field.type(indexPatternName, { charByChar: true }); + } else if (indexPatternName.charAt(indexPatternName.length - 1) === '*') { + // the common case where the UI will append '*' automatically so we won't type it + const tempName = indexPatternName.slice(0, -1); + await field.type(tempName, { charByChar: true }); + } else { + // case where we don't want the * appended so we'll remove it if it was added + await field.type(indexPatternName, { charByChar: true }); + const tempName = await field.getAttribute('value'); + if (tempName.length > indexPatternName.length) { + await field.type(browser.keys.DELETE, { charByChar: true }); + } + } const currentName = await field.getAttribute('value'); log.debug(`setIndexPatternField set to ${currentName}`); - expect(currentName).to.eql(`${indexPatternName}${expectWildcard ? '*' : ''}`); + expect(currentName).to.eql(indexPatternName); } async getCreateIndexPatternGoToStep2Button() { diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index 2bbc39969370b..fcf7298c5577a 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -76,7 +76,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { } it('should show correct node labels', async function() { - await PageObjects.graph.selectIndexPattern('secrepo*'); + await PageObjects.graph.selectIndexPattern('secrepo'); await buildGraph(); const { nodes } = await PageObjects.graph.getGraphObjects(); const circlesText = nodes.map(({ label }) => label); @@ -120,7 +120,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { it('should create new Graph workspace', async function() { await PageObjects.graph.newGraph(); - await PageObjects.graph.selectIndexPattern('secrepo*'); + await PageObjects.graph.selectIndexPattern('secrepo'); const { nodes, edges } = await PageObjects.graph.getGraphObjects(); expect(nodes).to.be.empty(); expect(edges).to.be.empty(); From fc2f2446eb73c1af3119db60bdf65d96102de593 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 30 Apr 2020 20:08:30 +0200 Subject: [PATCH 043/122] fix: fix migration (#64894) (cherry picked from commit 99b5982f1c6e0940acbe578d735aae31ddc57981) Co-authored-by: Elastic Machine --- x-pack/plugins/lens/server/migrations.test.ts | 2 +- x-pack/plugins/lens/server/migrations.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts index 4cc330d40efd7..0541d9636577b 100644 --- a/x-pack/plugins/lens/server/migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations.test.ts @@ -160,7 +160,7 @@ describe('Lens migrations', () => { }); describe('7.8.0 auto timestamp', () => { - const context = {} as SavedObjectMigrationContext; + const context = ({ log: { warning: () => {} } } as unknown) as SavedObjectMigrationContext; const example = { type: 'lens', diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts index 583fba1a4a999..a15e2b3692d02 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep, flow } from 'lodash'; +import { cloneDeep } from 'lodash'; import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { SavedObjectMigrationFn } from 'src/core/server'; @@ -156,5 +156,5 @@ export const migrations: Record> = { }, // The order of these migrations matter, since the timefield migration relies on the aggConfigs // sitting directly on the esaggs as an argument and not a nested function (which lens_auto_date was). - '7.8.0': flow(removeLensAutoDate, addTimeFieldToEsaggs), + '7.8.0': (doc, context) => addTimeFieldToEsaggs(removeLensAutoDate(doc, context), context), }; From 8a304623d4bf2e3be16f4c2cd25aa734b0308f5e Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Thu, 30 Apr 2020 11:12:22 -0700 Subject: [PATCH 044/122] [Ingest] Agent config settings UI (#64854) * Remove duplicate donut chart, move used donut chart closer to usage, clean up import paths * Change delete agent config to only single delete and delete all its associated data sources * Initial pass at settings tab * Reuse existing create agent config form instead * Fix delete api * Prevent nav drawer from hiding bottom bar (save action area) content * Remove delete config functionality from list page * Prevent API from deleting config with agents enrolled * Fix namespace populating in form * Display confirmation modal to deploy to agents if agents are detected * Adjust confirm delete copy * Fix i18n checks * Fix type check * Fix it again * De-dupe confirm modal * Fix i18n * Update agent config info after saving * Adjust skip unassign from agent config option schema in delete datasource method --- .../common/types/rest_spec/agent_config.ts | 8 +- .../hooks/use_request/agent_config.ts | 8 +- .../applications/ingest_manager/index.scss | 14 + .../applications/ingest_manager/index.tsx | 1 + .../components/config_delete_provider.tsx | 172 +++---- .../agent_config/components/config_form.tsx | 449 ++++++++++-------- .../confirm_deploy_modal.tsx} | 16 +- .../sections/agent_config/components/index.ts | 1 + .../components/linked_agent_count.tsx | 4 +- .../components/index.ts | 1 - .../create_datasource_page/index.tsx | 28 +- .../details_page/components/config_form.tsx | 94 ---- .../details_page/components/donut_chart.tsx | 65 --- .../details_page/components/edit_config.tsx | 135 ------ .../details_page/components/index.ts | 2 - .../components/settings/index.tsx | 216 +++++++++ .../details_page/components/yaml/index.tsx | 2 +- .../agent_config/details_page/hooks/index.ts | 2 +- .../agent_config/details_page/index.tsx | 33 +- .../edit_datasource_page/index.tsx | 8 +- .../sections/fleet/agent_list_page/index.scss | 6 - .../components/donut_chart.tsx | 0 .../sections/fleet/components/list_layout.tsx | 6 +- .../ingest_manager/types/index.ts | 4 +- .../server/routes/agent_config/handlers.ts | 12 +- .../server/routes/agent_config/index.ts | 4 +- .../server/services/agent_config.ts | 57 ++- .../server/services/datasource.ts | 20 +- .../server/types/rest_spec/agent_config.ts | 4 +- .../translations/translations/ja-JP.json | 26 - .../translations/translations/zh-CN.json | 26 - 31 files changed, 680 insertions(+), 744 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page/components/confirm_modal.tsx => components/confirm_deploy_modal.tsx} (76%) delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/config_form.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/donut_chart.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/edit_config.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/settings/index.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.scss rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/{agent_list_page => }/components/donut_chart.tsx (100%) diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts index 89d548d11dadb..82d7fa51b2082 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts @@ -49,16 +49,16 @@ export interface UpdateAgentConfigResponse { success: boolean; } -export interface DeleteAgentConfigsRequest { +export interface DeleteAgentConfigRequest { body: { - agentConfigIds: string[]; + agentConfigId: string; }; } -export type DeleteAgentConfigsResponse = Array<{ +export interface DeleteAgentConfigResponse { id: string; success: boolean; -}>; +} export interface GetFullAgentConfigRequest { params: { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts index bed3f994005ad..f80c468677f48 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts @@ -18,8 +18,8 @@ import { CreateAgentConfigResponse, UpdateAgentConfigRequest, UpdateAgentConfigResponse, - DeleteAgentConfigsRequest, - DeleteAgentConfigsResponse, + DeleteAgentConfigRequest, + DeleteAgentConfigResponse, } from '../../types'; export const useGetAgentConfigs = (query: HttpFetchQuery = {}) => { @@ -75,8 +75,8 @@ export const sendUpdateAgentConfig = ( }); }; -export const sendDeleteAgentConfigs = (body: DeleteAgentConfigsRequest['body']) => { - return sendRequest({ +export const sendDeleteAgentConfig = (body: DeleteAgentConfigRequest['body']) => { + return sendRequest({ path: agentConfigRouteService.getDeletePath(), method: 'post', body: JSON.stringify(body), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss new file mode 100644 index 0000000000000..fb95b1fa8bbfc --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss @@ -0,0 +1,14 @@ +@import '@elastic/eui/src/components/header/variables'; +@import '@elastic/eui/src/components/nav_drawer/variables'; + +/** + * 1. Hack EUI so the bottom bar doesn't obscure the nav drawer flyout. + */ +.ingestManager__bottomBar { + z-index: 0; /* 1 */ + left: $euiNavDrawerWidthCollapsed; +} + +.ingestManager__bottomBar-isNavDrawerLocked { + left: $euiNavDrawerWidthExpanded; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 6485862830d8a..295a35693726f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -23,6 +23,7 @@ import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp import { CoreContext, DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks'; import { PackageInstallProvider } from './sections/epm/hooks'; import { sendSetup } from './hooks/use_request/setup'; +import './index.scss'; export interface ProtectedRouteProps extends RouteProps { isAllowed?: boolean; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx index 9ae8369abbd52..d517dde45d5e3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx @@ -5,117 +5,92 @@ */ import React, { Fragment, useRef, useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal, EuiOverlayMask, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; -import { sendDeleteAgentConfigs, useCore, sendRequest } from '../../../hooks'; +import { sendDeleteAgentConfig, useCore, useConfig, sendRequest } from '../../../hooks'; interface Props { - children: (deleteAgentConfigs: deleteAgentConfigs) => React.ReactElement; + children: (deleteAgentConfig: DeleteAgentConfig) => React.ReactElement; } -export type deleteAgentConfigs = (agentConfigs: string[], onSuccess?: OnSuccessCallback) => void; +export type DeleteAgentConfig = (agentConfig: string, onSuccess?: OnSuccessCallback) => void; -type OnSuccessCallback = (agentConfigsUnenrolled: string[]) => void; +type OnSuccessCallback = (agentConfigDeleted: string) => void; export const AgentConfigDeleteProvider: React.FunctionComponent = ({ children }) => { const { notifications } = useCore(); - const [agentConfigs, setAgentConfigs] = useState([]); + const { + fleet: { enabled: isFleetEnabled }, + } = useConfig(); + const [agentConfig, setAgentConfig] = useState(); const [isModalOpen, setIsModalOpen] = useState(false); const [isLoadingAgentsCount, setIsLoadingAgentsCount] = useState(false); const [agentsCount, setAgentsCount] = useState(0); const [isLoading, setIsLoading] = useState(false); const onSuccessCallback = useRef(null); - const deleteAgentConfigsPrompt: deleteAgentConfigs = ( - agentConfigsToDelete, + const deleteAgentConfigPrompt: DeleteAgentConfig = ( + agentConfigToDelete, onSuccess = () => undefined ) => { - if ( - agentConfigsToDelete === undefined || - (Array.isArray(agentConfigsToDelete) && agentConfigsToDelete.length === 0) - ) { - throw new Error('No agent configs specified for deletion'); + if (!agentConfigToDelete) { + throw new Error('No agent config specified for deletion'); } setIsModalOpen(true); - setAgentConfigs(agentConfigsToDelete); - fetchAgentsCount(agentConfigsToDelete); + setAgentConfig(agentConfigToDelete); + fetchAgentsCount(agentConfigToDelete); onSuccessCallback.current = onSuccess; }; const closeModal = () => { - setAgentConfigs([]); + setAgentConfig(undefined); setIsLoading(false); setIsLoadingAgentsCount(false); setIsModalOpen(false); }; - const deleteAgentConfigs = async () => { + const deleteAgentConfig = async () => { setIsLoading(true); try { - const { data } = await sendDeleteAgentConfigs({ - agentConfigIds: agentConfigs, + const { data } = await sendDeleteAgentConfig({ + agentConfigId: agentConfig!, }); - const successfulResults = data?.filter(result => result.success) || []; - const failedResults = data?.filter(result => !result.success) || []; - if (successfulResults.length) { - const hasMultipleSuccesses = successfulResults.length > 1; - const successMessage = hasMultipleSuccesses - ? i18n.translate( - 'xpack.ingestManager.deleteAgentConfigs.successMultipleNotificationTitle', - { - defaultMessage: 'Deleted {count} agent configs', - values: { count: successfulResults.length }, - } - ) - : i18n.translate( - 'xpack.ingestManager.deleteAgentConfigs.successSingleNotificationTitle', - { - defaultMessage: "Deleted agent config '{id}'", - values: { id: successfulResults[0].id }, - } - ); - notifications.toasts.addSuccess(successMessage); + if (data?.success) { + notifications.toasts.addSuccess( + i18n.translate('xpack.ingestManager.deleteAgentConfig.successSingleNotificationTitle', { + defaultMessage: "Deleted agent config '{id}'", + values: { id: agentConfig }, + }) + ); + if (onSuccessCallback.current) { + onSuccessCallback.current(agentConfig!); + } } - if (failedResults.length) { - const hasMultipleFailures = failedResults.length > 1; - const failureMessage = hasMultipleFailures - ? i18n.translate( - 'xpack.ingestManager.deleteAgentConfigs.failureMultipleNotificationTitle', - { - defaultMessage: 'Error deleting {count} agent configs', - values: { count: failedResults.length }, - } - ) - : i18n.translate( - 'xpack.ingestManager.deleteAgentConfigs.failureSingleNotificationTitle', - { - defaultMessage: "Error deleting agent config '{id}'", - values: { id: failedResults[0].id }, - } - ); - notifications.toasts.addDanger(failureMessage); - } - - if (onSuccessCallback.current) { - onSuccessCallback.current(successfulResults.map(result => result.id)); + if (!data?.success) { + notifications.toasts.addDanger( + i18n.translate('xpack.ingestManager.deleteAgentConfig.failureSingleNotificationTitle', { + defaultMessage: "Error deleting agent config '{id}'", + values: { id: agentConfig }, + }) + ); } } catch (e) { notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.deleteAgentConfigs.fatalErrorNotificationTitle', { - defaultMessage: 'Error deleting agent configs', + i18n.translate('xpack.ingestManager.deleteAgentConfig.fatalErrorNotificationTitle', { + defaultMessage: 'Error deleting agent config', }) ); } closeModal(); }; - const fetchAgentsCount = async (agentConfigsToCheck: string[]) => { - if (isLoadingAgentsCount) { + const fetchAgentsCount = async (agentConfigToCheck: string) => { + if (!isFleetEnabled || isLoadingAgentsCount) { return; } setIsLoadingAgentsCount(true); @@ -123,7 +98,7 @@ export const AgentConfigDeleteProvider: React.FunctionComponent = ({ chil path: `/api/ingest_manager/fleet/agents`, method: 'get', query: { - kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id : (${agentConfigsToCheck.join(' or ')})`, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id : ${agentConfigToCheck}`, }, }); setAgentsCount(data?.total || 0); @@ -140,68 +115,61 @@ export const AgentConfigDeleteProvider: React.FunctionComponent = ({ chil } onCancel={closeModal} - onConfirm={deleteAgentConfigs} + onConfirm={deleteAgentConfig} cancelButtonText={ } confirmButtonText={ isLoading || isLoadingAgentsCount ? ( - ) : agentsCount ? ( - ) : ( ) } buttonColor="danger" - confirmButtonDisabled={isLoading || isLoadingAgentsCount} + confirmButtonDisabled={isLoading || isLoadingAgentsCount || !!agentsCount} > {isLoadingAgentsCount ? ( ) : agentsCount ? ( - + + + ) : ( )} @@ -211,7 +179,7 @@ export const AgentConfigDeleteProvider: React.FunctionComponent = ({ chil return ( - {children(deleteAgentConfigsPrompt)} + {children(deleteAgentConfigPrompt)} {renderModal()} ); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx index 92c44d86e47c6..c55d6009074b0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx @@ -8,8 +8,7 @@ import React, { useMemo, useState } from 'react'; import { EuiAccordion, EuiFieldText, - EuiFlexGroup, - EuiFlexItem, + EuiDescribedFormGroup, EuiForm, EuiFormRow, EuiHorizontalRule, @@ -19,11 +18,13 @@ import { EuiComboBox, EuiIconTip, EuiCheckboxGroup, + EuiButton, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; -import { NewAgentConfig } from '../../../types'; +import { NewAgentConfig, AgentConfig } from '../../../types'; +import { AgentConfigDeleteProvider } from './config_delete_provider'; interface ValidationResults { [key: string]: JSX.Element[]; @@ -36,7 +37,7 @@ const StyledEuiAccordion = styled(EuiAccordion)` `; export const agentConfigFormValidation = ( - agentConfig: Partial + agentConfig: Partial ): ValidationResults => { const errors: ValidationResults = {}; @@ -53,11 +54,13 @@ export const agentConfigFormValidation = ( }; interface Props { - agentConfig: Partial; - updateAgentConfig: (u: Partial) => void; + agentConfig: Partial; + updateAgentConfig: (u: Partial) => void; withSysMonitoring: boolean; updateSysMonitoring: (newValue: boolean) => void; validation: ValidationResults; + isEditing?: boolean; + onDelete?: () => void; } export const AgentConfigForm: React.FunctionComponent = ({ @@ -66,9 +69,11 @@ export const AgentConfigForm: React.FunctionComponent = ({ withSysMonitoring, updateSysMonitoring, validation, + isEditing = false, + onDelete = () => {}, }) => { const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({}); - const [showNamespace, setShowNamespace] = useState(false); + const [showNamespace, setShowNamespace] = useState(!!agentConfig.namespace); const fields: Array<{ name: 'name' | 'description' | 'namespace'; label: JSX.Element; @@ -105,209 +110,281 @@ export const AgentConfigForm: React.FunctionComponent = ({ ]; }, []); - return ( - - {fields.map(({ name, label, placeholder }) => { - return ( - - updateAgentConfig({ [name]: e.target.value })} - isInvalid={Boolean(touchedFields[name] && validation[name])} - onBlur={() => setTouchedFields({ ...touchedFields, [name]: true })} - placeholder={placeholder} - /> - - ); - })} + const generalSettingsWrapper = (children: JSX.Element[]) => ( + + + + } + description={ + + } + > + {children} + + ); + + const generalFields = fields.map(({ name, label, placeholder }) => { + return ( + fullWidth + key={name} + label={label} + error={touchedFields[name] && validation[name] ? validation[name] : null} + isInvalid={Boolean(touchedFields[name] && validation[name])} + > + updateAgentConfig({ [name]: e.target.value })} + isInvalid={Boolean(touchedFields[name] && validation[name])} + onBlur={() => setTouchedFields({ ...touchedFields, [name]: true })} + placeholder={placeholder} + /> + + ); + }); + + const advancedOptionsContent = ( + <> + - + + } + description={ + } > - {' '} - - + } - checked={withSysMonitoring} + checked={showNamespace} onChange={() => { - updateSysMonitoring(!withSysMonitoring); + setShowNamespace(!showNamespace); + if (showNamespace) { + updateAgentConfig({ namespace: '' }); + } }} /> - - - - + + + { + updateAgentConfig({ namespace: value }); + }} + onChange={selectedOptions => { + updateAgentConfig({ + namespace: (selectedOptions.length ? selectedOptions[0] : '') as string, + }); + }} + isInvalid={Boolean(touchedFields.namespace && validation.namespace)} + onBlur={() => setTouchedFields({ ...touchedFields, namespace: true })} + /> + + + )} + + + + + } + description={ } - buttonClassName="ingest-active-button" > - - - - -

- -

-
- - + { + acc[key] = true; + return acc; + }, + { logs: false, metrics: false } + )} + onChange={id => { + if (id !== 'logs' && id !== 'metrics') { + return; + } + + const hasLogs = + agentConfig.monitoring_enabled && agentConfig.monitoring_enabled.indexOf(id) >= 0; + + const previousValues = agentConfig.monitoring_enabled || []; + updateAgentConfig({ + monitoring_enabled: hasLogs + ? previousValues.filter(type => type !== id) + : [...previousValues, id], + }); + }} + /> +
+ {isEditing && 'id' in agentConfig ? ( + + + + } + description={ + <> + + + + {deleteAgentConfigPrompt => { + return ( + deleteAgentConfigPrompt(agentConfig.id!, onDelete)} + > + + + ); + }} + + {agentConfig.is_default ? ( + <> + + + + + + ) : null} + + } + /> + ) : null} + + ); + + return ( + + {!isEditing ? generalFields : generalSettingsWrapper(generalFields)} + {!isEditing ? ( + - - - - } - checked={showNamespace} - onChange={() => { - setShowNamespace(!showNamespace); - if (showNamespace) { - updateAgentConfig({ namespace: '' }); - } - }} - /> - {showNamespace && ( + } + > + - - - { - updateAgentConfig({ namespace: value }); - }} - onChange={selectedOptions => { - updateAgentConfig({ - namespace: (selectedOptions.length ? selectedOptions[0] : '') as string, - }); - }} - isInvalid={Boolean(touchedFields.namespace && validation.namespace)} - onBlur={() => setTouchedFields({ ...touchedFields, namespace: true })} - /> - - - )} - - - - - - -

{' '} + -

-
- - + + } + checked={withSysMonitoring} + onChange={() => { + updateSysMonitoring(!withSysMonitoring); + }} + /> +
+ ) : null} + {!isEditing ? ( + <> + + + - - - - { - acc[key] = true; - return acc; - }, - { logs: false, metrics: false } - )} - onChange={id => { - if (id !== 'logs' && id !== 'metrics') { - return; - } - - const hasLogs = - agentConfig.monitoring_enabled && agentConfig.monitoring_enabled.indexOf(id) >= 0; - - const previousValues = agentConfig.monitoring_enabled || []; - updateAgentConfig({ - monitoring_enabled: hasLogs - ? previousValues.filter(type => type !== id) - : [...previousValues, id], - }); - }} - /> - - - + } + buttonClassName="ingest-active-button" + > + + {advancedOptionsContent} +
+ + ) : ( + advancedOptionsContent + )} ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/confirm_modal.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/confirm_deploy_modal.tsx similarity index 76% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/confirm_modal.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/confirm_deploy_modal.tsx index aa7eab8f5be8d..a503beeffa8b4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/confirm_modal.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/confirm_deploy_modal.tsx @@ -8,9 +8,9 @@ import React from 'react'; import { EuiCallOut, EuiOverlayMask, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { AgentConfig } from '../../../../types'; +import { AgentConfig } from '../../../types'; -export const ConfirmCreateDatasourceModal: React.FunctionComponent<{ +export const ConfirmDeployConfigModal: React.FunctionComponent<{ onConfirm: () => void; onCancel: () => void; agentCount: number; @@ -21,7 +21,7 @@ export const ConfirmCreateDatasourceModal: React.FunctionComponent<{ } @@ -29,13 +29,13 @@ export const ConfirmCreateDatasourceModal: React.FunctionComponent<{ onConfirm={onConfirm} cancelButtonText={ } confirmButtonText={ } @@ -43,7 +43,7 @@ export const ConfirmCreateDatasourceModal: React.FunctionComponent<{ > diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts index a0fdc656dd7ed..c1811b99588a8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts @@ -7,3 +7,4 @@ export { AgentConfigForm, agentConfigFormValidation } from './config_form'; export { AgentConfigDeleteProvider } from './config_delete_provider'; export { LinkedAgentCount } from './linked_agent_count'; +export { ConfirmDeployConfigModal } from './confirm_deploy_modal'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/linked_agent_count.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/linked_agent_count.tsx index ec66108c60f68..3860439f26d44 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/linked_agent_count.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/linked_agent_count.tsx @@ -8,7 +8,7 @@ import React, { memo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiLink } from '@elastic/eui'; import { useLink } from '../../../hooks'; -import { FLEET_AGENTS_PATH } from '../../../constants'; +import { FLEET_AGENTS_PATH, AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; export const LinkedAgentCount = memo<{ count: number; agentConfigId: string }>( ({ count, agentConfigId }) => { @@ -21,7 +21,7 @@ export const LinkedAgentCount = memo<{ count: number; agentConfigId: string }>( /> ); return count > 0 ? ( - + {displayValue} ) : ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts index aa564690a6092..3bfca75668911 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts @@ -5,5 +5,4 @@ */ export { CreateDatasourcePageLayout } from './layout'; export { DatasourceInputPanel } from './datasource_input_panel'; -export { ConfirmCreateDatasourceModal } from './confirm_modal'; export { DatasourceInputVarField } from './datasource_input_var_field'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 8e7042c1275ad..5b7553dd8cf92 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -27,7 +27,8 @@ import { sendGetAgentStatus, } from '../../../hooks'; import { useLinks as useEPMLinks } from '../../epm/hooks'; -import { CreateDatasourcePageLayout, ConfirmCreateDatasourceModal } from './components'; +import { ConfirmDeployConfigModal } from '../components'; +import { CreateDatasourcePageLayout } from './components'; import { CreateDatasourceFrom, DatasourceFormState } from './types'; import { DatasourceValidationResults, validateDatasource, validationHasErrors } from './services'; import { StepSelectPackage } from './step_select_package'; @@ -36,7 +37,10 @@ import { StepConfigureDatasource } from './step_configure_datasource'; import { StepDefineDatasource } from './step_define_datasource'; export const CreateDatasourcePage: React.FunctionComponent = () => { - const { notifications } = useCore(); + const { + notifications, + chrome: { getIsNavDrawerLocked$ }, + } = useCore(); const { fleet: { enabled: isFleetEnabled }, } = useConfig(); @@ -45,6 +49,15 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } = useRouteMatch(); const history = useHistory(); const from: CreateDatasourceFrom = configId ? 'config' : 'package'; + const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); + + useEffect(() => { + const subscription = getIsNavDrawerLocked$().subscribe((newIsNavDrawerLocked: boolean) => { + setIsNavDrawerLocked(newIsNavDrawerLocked); + }); + + return () => subscription.unsubscribe(); + }); // Agent config and package info states const [agentConfig, setAgentConfig] = useState(); @@ -269,7 +282,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { return ( {formState === 'CONFIRM' && agentConfig && ( - { )} - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/config_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/config_form.tsx deleted file mode 100644 index c4f8d944ceb14..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/config_form.tsx +++ /dev/null @@ -1,94 +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, { useState } from 'react'; -import { EuiFieldText, EuiForm, EuiFormRow } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { AgentConfig } from '../../../../types'; - -interface ValidationResults { - [key: string]: JSX.Element[]; -} - -export const configFormValidation = (config: Partial): ValidationResults => { - const errors: ValidationResults = {}; - - if (!config.name?.trim()) { - errors.name = [ - , - ]; - } - - return errors; -}; - -interface Props { - config: Partial; - updateConfig: (u: Partial) => void; - validation: ValidationResults; -} - -export const ConfigForm: React.FunctionComponent = ({ - config, - updateConfig, - validation, -}) => { - const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({}); - const fields: Array<{ name: 'name' | 'description' | 'namespace'; label: JSX.Element }> = [ - { - name: 'name', - label: ( - - ), - }, - { - name: 'description', - label: ( - - ), - }, - { - name: 'namespace', - label: ( - - ), - }, - ]; - - return ( - - {fields.map(({ name, label }) => { - return ( - - updateConfig({ [name]: e.target.value })} - isInvalid={Boolean(touchedFields[name] && validation[name])} - onBlur={() => setTouchedFields({ ...touchedFields, [name]: true })} - /> - - ); - })} - - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/donut_chart.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/donut_chart.tsx deleted file mode 100644 index 408ccc6e951f6..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/donut_chart.tsx +++ /dev/null @@ -1,65 +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, { useEffect, useRef } from 'react'; -import d3 from 'd3'; -import { EuiFlexItem } from '@elastic/eui'; - -interface DonutChartProps { - data: { - [key: string]: number; - }; - height: number; - width: number; -} - -export const DonutChart = ({ height, width, data }: DonutChartProps) => { - const chartElement = useRef(null); - - useEffect(() => { - if (chartElement.current !== null) { - // we must remove any existing paths before painting - d3.selectAll('g').remove(); - const svgElement = d3 - .select(chartElement.current) - .append('g') - .attr('transform', `translate(${width / 2}, ${height / 2})`); - const color = d3.scale - .ordinal() - // @ts-ignore - .domain(data) - .range(['#017D73', '#98A2B3', '#BD271E']); - const pieGenerator = d3.layout - .pie() - .value(({ value }: any) => value) - // these start/end angles will reverse the direction of the pie, - // which matches our design - .startAngle(2 * Math.PI) - .endAngle(0); - - svgElement - .selectAll('g') - // @ts-ignore - .data(pieGenerator(d3.entries(data))) - .enter() - .append('path') - .attr( - 'd', - // @ts-ignore attr does not expect a param of type Arc but it behaves as desired - d3.svg - .arc() - .innerRadius(width * 0.28) - .outerRadius(Math.min(width, height) / 2 - 10) - ) - .attr('fill', (d: any) => color(d.data.key)); - } - }, [data, height, width]); - return ( - - - - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/edit_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/edit_config.tsx deleted file mode 100644 index 65eb86d7d871f..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/edit_config.tsx +++ /dev/null @@ -1,135 +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, { useState } from 'react'; -import { - EuiFlyout, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiButton, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useCore, sendRequest } from '../../../../hooks'; -import { agentConfigRouteService } from '../../../../services'; -import { AgentConfig } from '../../../../types'; -import { ConfigForm, configFormValidation } from './config_form'; - -interface Props { - agentConfig: AgentConfig; - onClose: () => void; -} - -export const EditConfigFlyout: React.FunctionComponent = ({ - agentConfig: originalAgentConfig, - onClose, -}) => { - const { notifications } = useCore(); - const [config, setConfig] = useState>({ - name: originalAgentConfig.name, - description: originalAgentConfig.description, - }); - const [isLoading, setIsLoading] = useState(false); - const updateConfig = (updatedFields: Partial) => { - setConfig({ - ...config, - ...updatedFields, - }); - }; - const validation = configFormValidation(config); - - const header = ( - - -

- -

-
-
- ); - - const body = ( - - - - ); - - const footer = ( - - - - - - - - - 0} - onClick={async () => { - setIsLoading(true); - try { - const { error } = await sendRequest({ - path: agentConfigRouteService.getUpdatePath(originalAgentConfig.id), - method: 'put', - body: JSON.stringify(config), - }); - if (!error) { - notifications.toasts.addSuccess( - i18n.translate('xpack.ingestManager.editConfig.successNotificationTitle', { - defaultMessage: "Agent config '{name}' updated", - values: { name: config.name }, - }) - ); - } else { - notifications.toasts.addDanger( - error - ? error.message - : i18n.translate('xpack.ingestManager.editConfig.errorNotificationTitle', { - defaultMessage: 'Unable to update agent config', - }) - ); - } - } catch (e) { - notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.editConfig.errorNotificationTitle', { - defaultMessage: 'Unable to update agent config', - }) - ); - } - setIsLoading(false); - onClose(); - }} - > - - - - - - ); - - return ( - - {header} - {body} - {footer} - - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts index 918b361a60d79..0123bd46c16e7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts @@ -4,5 +4,3 @@ * you may not use this file except in compliance with the Elastic License. */ export { DatasourcesTable } from './datasources/datasources_table'; -export { DonutChart } from './donut_chart'; -export { EditConfigFlyout } from './edit_config'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/settings/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/settings/index.tsx new file mode 100644 index 0000000000000..2d9d29bfc1ac7 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/settings/index.tsx @@ -0,0 +1,216 @@ +/* + * 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, { memo, useState, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import styled from 'styled-components'; +import { EuiBottomBar, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AGENT_CONFIG_PATH } from '../../../../../constants'; +import { AgentConfig } from '../../../../../types'; +import { + useCore, + useCapabilities, + sendUpdateAgentConfig, + useConfig, + sendGetAgentStatus, +} from '../../../../../hooks'; +import { + AgentConfigForm, + agentConfigFormValidation, + ConfirmDeployConfigModal, +} from '../../../components'; +import { useConfigRefresh } from '../../hooks'; + +const FormWrapper = styled.div` + max-width: 800px; + margin-right: auto; + margin-left: auto; +`; + +export const ConfigSettingsView = memo<{ config: AgentConfig }>( + ({ config: originalAgentConfig }) => { + const { + notifications, + chrome: { getIsNavDrawerLocked$ }, + } = useCore(); + const { + fleet: { enabled: isFleetEnabled }, + } = useConfig(); + const history = useHistory(); + const hasWriteCapabilites = useCapabilities().write; + const refreshConfig = useConfigRefresh(); + const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); + const [agentConfig, setAgentConfig] = useState({ + ...originalAgentConfig, + }); + const [isLoading, setIsLoading] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + const [agentCount, setAgentCount] = useState(0); + const [withSysMonitoring, setWithSysMonitoring] = useState(true); + const validation = agentConfigFormValidation(agentConfig); + + useEffect(() => { + const subscription = getIsNavDrawerLocked$().subscribe((newIsNavDrawerLocked: boolean) => { + setIsNavDrawerLocked(newIsNavDrawerLocked); + }); + + return () => subscription.unsubscribe(); + }); + + const updateAgentConfig = (updatedFields: Partial) => { + setAgentConfig({ + ...agentConfig, + ...updatedFields, + }); + setHasChanges(true); + }; + + const submitUpdateAgentConfig = async () => { + setIsLoading(true); + try { + const { name, description, namespace, monitoring_enabled } = agentConfig; + const { data, error } = await sendUpdateAgentConfig(agentConfig.id, { + name, + description, + namespace, + monitoring_enabled, + }); + if (data?.success) { + notifications.toasts.addSuccess( + i18n.translate('xpack.ingestManager.editAgentConfig.successNotificationTitle', { + defaultMessage: "Successfully updated '{name}' settings", + values: { name: agentConfig.name }, + }) + ); + refreshConfig(); + setHasChanges(false); + } else { + notifications.toasts.addDanger( + error + ? error.message + : i18n.translate('xpack.ingestManager.editAgentConfig.errorNotificationTitle', { + defaultMessage: 'Unable to update agent config', + }) + ); + } + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('xpack.ingestManager.editAgentConfig.errorNotificationTitle', { + defaultMessage: 'Unable to update agent config', + }) + ); + } + setIsLoading(false); + }; + + const onSubmit = async () => { + // Retrieve agent count if fleet is enabled + if (isFleetEnabled) { + setIsLoading(true); + const { data } = await sendGetAgentStatus({ configId: agentConfig.id }); + if (data?.results.total) { + setAgentCount(data.results.total); + } else { + await submitUpdateAgentConfig(); + } + } else { + await submitUpdateAgentConfig(); + } + }; + + return ( + + {agentCount ? ( + { + setAgentCount(0); + submitUpdateAgentConfig(); + }} + onCancel={() => { + setAgentCount(0); + setIsLoading(false); + }} + /> + ) : null} + setWithSysMonitoring(newValue)} + validation={validation} + isEditing={true} + onDelete={() => { + history.push(AGENT_CONFIG_PATH); + }} + /> + {hasChanges ? ( + + + + + + + + + { + setAgentConfig({ ...originalAgentConfig }); + setHasChanges(false); + }} + > + + + + + 0 + } + iconType="save" + color="primary" + fill + > + {isLoading ? ( + + ) : ( + + )} + + + + + + + ) : null} + + ); + } +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx index f1d7bd5dbc039..9f2088521ed38 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx @@ -15,7 +15,7 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import { AgentConfig } from '../../../../../../../../common/types/models'; +import { AgentConfig } from '../../../../../types'; import { useGetOneAgentConfigFull, useGetEnrollmentAPIKeys, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/index.ts index 19be93676a734..76c6d64eb9e07 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ export { useGetAgentStatus, AgentStatusRefreshContext } from './use_agent_status'; -export { ConfigRefreshContext } from './use_config'; +export { ConfigRefreshContext, useConfigRefresh } from './use_config'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx index 450f86df5c03a..20a39724ce23c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx @@ -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 React, { Fragment, memo, useCallback, useMemo, useState } from 'react'; +import React, { Fragment, memo, useMemo, useState } from 'react'; import { Redirect, useRouteMatch, Switch, Route } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; @@ -26,12 +26,12 @@ import { useGetOneAgentConfig } from '../../../hooks'; import { Loading } from '../../../components'; import { WithHeaderLayout } from '../../../layouts'; import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; -import { EditConfigFlyout } from './components'; import { LinkedAgentCount } from '../components'; import { useAgentConfigLink } from './hooks/use_details_uri'; import { DETAILS_ROUTER_PATH, DETAILS_ROUTER_SUB_PATH } from './constants'; import { ConfigDatasourcesView } from './components/datasources'; import { ConfigYamlView } from './components/yaml'; +import { ConfigSettingsView } from './components/settings'; const Divider = styled.div` width: 0; @@ -70,14 +70,6 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { const configDetailsYamlLink = useAgentConfigLink('details-yaml', { configId }); const configDetailsSettingsLink = useAgentConfigLink('details-settings', { configId }); - // Flyout states - const [isEditConfigFlyoutOpen, setIsEditConfigFlyoutOpen] = useState(false); - - const refreshData = useCallback(() => { - refreshAgentConfig(); - refreshAgentStatus(); - }, [refreshAgentConfig, refreshAgentStatus]); - const headerLeftContent = useMemo( () => ( @@ -196,7 +188,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { return [ { id: 'datasources', - name: i18n.translate('xpack.ingestManager.configDetails.subTabs.datasouces', { + name: i18n.translate('xpack.ingestManager.configDetails.subTabs.datasourcesTabText', { defaultMessage: 'Data sources', }), href: configDetailsLink, @@ -204,15 +196,15 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { }, { id: 'yaml', - name: i18n.translate('xpack.ingestManager.configDetails.subTabs.yamlFile', { - defaultMessage: 'YAML File', + name: i18n.translate('xpack.ingestManager.configDetails.subTabs.yamlTabText', { + defaultMessage: 'YAML', }), href: configDetailsYamlLink, isSelected: tabId === 'yaml', }, { id: 'settings', - name: i18n.translate('xpack.ingestManager.configDetails.subTabs.settings', { + name: i18n.translate('xpack.ingestManager.configDetails.subTabs.settingsTabText', { defaultMessage: 'Settings', }), href: configDetailsSettingsLink, @@ -269,16 +261,6 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { rightColumn={headerRightContent} tabs={(headerTabs as unknown) as EuiTabProps[]} > - {isEditConfigFlyoutOpen ? ( - { - setIsEditConfigFlyoutOpen(false); - refreshData(); - }} - agentConfig={agentConfig} - /> - ) : null} - { { - // TODO: Settings implementation tracked via: https://github.com/elastic/kibana/issues/57959 - return
Settings placeholder
; + return ; }} /> { ) : ( <> {formState === 'CONFIRM' && ( - listAgents(soClient, { - showInactive: true, + showInactive: false, perPage: 0, page: 1, kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:${agentConfig.id}`, @@ -179,13 +179,13 @@ export const updateAgentConfigHandler: RequestHandler< export const deleteAgentConfigsHandler: RequestHandler< unknown, unknown, - TypeOf + TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { - const body: DeleteAgentConfigsResponse = await agentConfigService.delete( + const body: DeleteAgentConfigResponse = await agentConfigService.delete( soClient, - request.body.agentConfigIds + request.body.agentConfigId ); return response.ok({ body, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts index b8e827974ff81..e630f3c959590 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts @@ -10,7 +10,7 @@ import { GetOneAgentConfigRequestSchema, CreateAgentConfigRequestSchema, UpdateAgentConfigRequestSchema, - DeleteAgentConfigsRequestSchema, + DeleteAgentConfigRequestSchema, GetFullAgentConfigRequestSchema, } from '../../types'; import { @@ -67,7 +67,7 @@ export const registerRoutes = (router: IRouter) => { router.post( { path: AGENT_CONFIG_API_ROUTES.DELETE_PATTERN, - validate: DeleteAgentConfigsRequestSchema, + validate: DeleteAgentConfigRequestSchema, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, deleteAgentConfigsHandler diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index 7ab6ef1920c18..5ecbaff8ad71e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -6,7 +6,11 @@ import { uniq } from 'lodash'; import { SavedObjectsClientContract } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; -import { DEFAULT_AGENT_CONFIG, AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../constants'; +import { + DEFAULT_AGENT_CONFIG, + AGENT_CONFIG_SAVED_OBJECT_TYPE, + AGENT_SAVED_OBJECT_TYPE, +} from '../constants'; import { Datasource, NewAgentConfig, @@ -15,7 +19,8 @@ import { AgentConfigStatus, ListWithKuery, } from '../types'; -import { DeleteAgentConfigsResponse, storedDatasourceToAgentDatasource } from '../../common'; +import { DeleteAgentConfigResponse, storedDatasourceToAgentDatasource } from '../../common'; +import { listAgents } from './agents'; import { datasourceService } from './datasource'; import { outputService } from './output'; import { agentConfigUpdateEventHandler } from './agent_config_update'; @@ -256,32 +261,40 @@ class AgentConfigService { public async delete( soClient: SavedObjectsClientContract, - ids: string[] - ): Promise { - const result: DeleteAgentConfigsResponse = []; - const defaultConfigId = await this.getDefaultAgentConfigId(soClient); + id: string + ): Promise { + const config = await this.get(soClient, id, false); + if (!config) { + throw new Error('Agent configuration not found'); + } - if (ids.includes(defaultConfigId)) { + const defaultConfigId = await this.getDefaultAgentConfigId(soClient); + if (id === defaultConfigId) { throw new Error('The default agent configuration cannot be deleted'); } - for (const id of ids) { - try { - await soClient.delete(SAVED_OBJECT_TYPE, id); - await this.triggerAgentConfigUpdatedEvent(soClient, 'deleted', id); - result.push({ - id, - success: true, - }); - } catch (e) { - result.push({ - id, - success: false, - }); - } + const { total } = await listAgents(soClient, { + showInactive: false, + perPage: 0, + page: 1, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:${id}`, + }); + + if (total > 0) { + throw new Error('Cannot delete agent config that is assigned to agent(s)'); } - return result; + if (config.datasources && config.datasources.length) { + await datasourceService.delete(soClient, config.datasources as string[], { + skipUnassignFromAgentConfigs: true, + }); + } + await soClient.delete(SAVED_OBJECT_TYPE, id); + await this.triggerAgentConfigUpdatedEvent(soClient, 'deleted', id); + return { + id, + success: true, + }; } public async getFullConfig( diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts index 0a5ba43e40fba..affd9b2755881 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.ts @@ -145,7 +145,7 @@ class DatasourceService { public async delete( soClient: SavedObjectsClientContract, ids: string[], - options?: { user?: AuthenticatedUser } + options?: { user?: AuthenticatedUser; skipUnassignFromAgentConfigs?: boolean } ): Promise { const result: DeleteDatasourcesResponse = []; @@ -155,14 +155,16 @@ class DatasourceService { if (!oldDatasource) { throw new Error('Datasource not found'); } - await agentConfigService.unassignDatasources( - soClient, - oldDatasource.config_id, - [oldDatasource.id], - { - user: options?.user, - } - ); + if (!options?.skipUnassignFromAgentConfigs) { + await agentConfigService.unassignDatasources( + soClient, + oldDatasource.config_id, + [oldDatasource.id], + { + user: options?.user, + } + ); + } await soClient.delete(SAVED_OBJECT_TYPE, id); result.push({ id, diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts index 0d223f028fc88..ab97ddc0ba723 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts @@ -29,9 +29,9 @@ export const UpdateAgentConfigRequestSchema = { body: NewAgentConfigSchema, }; -export const DeleteAgentConfigsRequestSchema = { +export const DeleteAgentConfigRequestSchema = { body: schema.object({ - agentConfigIds: schema.arrayOf(schema.string()), + agentConfigId: schema.string(), }), }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a3011ab5bdfa9..bf257d66a59bc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8317,9 +8317,6 @@ "xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "名前空間", "xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "パッケージ", "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "ストリーム", - "xpack.ingestManager.configDetails.subTabs.datasouces": "データソース", - "xpack.ingestManager.configDetails.subTabs.settings": "設定", - "xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML ファイル", "xpack.ingestManager.configDetails.summary.datasources": "データソース", "xpack.ingestManager.configDetails.summary.lastUpdated": "最終更新日:", "xpack.ingestManager.configDetails.summary.revision": "リビジョン", @@ -8329,10 +8326,6 @@ "xpack.ingestManager.configDetailsDatasources.createFirstButtonText": "データソースを作成", "xpack.ingestManager.configDetailsDatasources.createFirstMessage": "この構成にはデータソースはまだありません。", "xpack.ingestManager.configDetailsDatasources.createFirstTitle": "初めてのデーソースを作成する", - "xpack.ingestManager.configForm.descriptionFieldLabel": "説明", - "xpack.ingestManager.configForm.nameFieldLabel": "名前", - "xpack.ingestManager.configForm.nameRequiredErrorMessage": "構成名が必要です", - "xpack.ingestManager.configForm.namespaceFieldLabel": "名前空間", "xpack.ingestManager.createAgentConfig.cancelButtonLabel": "キャンセル", "xpack.ingestManager.createAgentConfig.errorNotificationTitle": "エージェント構成を作成できません", "xpack.ingestManager.createAgentConfig.flyoutTitle": "エージェント構成を作成", @@ -8364,20 +8357,6 @@ "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingPackagesTitle": "パッケージの読み込みエラー", "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingSelectedPackageTitle": "選択したパッケージの読み込みエラー", "xpack.ingestManager.createDatasource.stepSelectPackage.filterPackagesInputPlaceholder": "パッケージの検索", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# エージェントを} other {# エージェントを}}{agentConfigsCount, plural, one {このエージェント構成に} other {これらのエージェント構成に}}割り当てました。 {agentsCount, plural, one {このエージェント} other {これらのエージェント}}の登録が解除されます。", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmAndReassignButtonLabel": "{agentConfigsCount, plural, one {エージェント構成} other {エージェント構成}} and unenroll {agentsCount, plural, one {エージェント} other {エージェント}} を削除", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmButtonLabel": "{agentConfigsCount, plural, one {エージェント構成} other {エージェント構成}}を削除", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.deleteMultipleTitle": "{count, plural, one {this agent config} other {# agent configs}} を削除しますか?", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingAgentsCountMessage": "影響があるエージェントの数を確認中...", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingButtonLabel": "読み込み中...", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.noAffectedAgentsMessage": "{agentConfigsCount, plural, one {this agent config} other {these agentConfigs}}に割り当てられたエージェントはありません。", - "xpack.ingestManager.deleteAgentConfigs.failureMultipleNotificationTitle": "{count} 件のエージェント構成の削除エラー", - "xpack.ingestManager.deleteAgentConfigs.failureSingleNotificationTitle": "エージェント構成「{id}」の削除エラー", - "xpack.ingestManager.deleteAgentConfigs.fatalErrorNotificationTitle": "エージェント構成の削除エラー", - "xpack.ingestManager.deleteAgentConfigs.successMultipleNotificationTitle": "{count} 件のエージェント構成を削除しました", - "xpack.ingestManager.deleteAgentConfigs.successSingleNotificationTitle": "エージェント構成「{id}」を削除しました", - "xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel": "キャンセル", "xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsMessage": "{agentConfigName} が一部のエージェントで既に使用されていることをフリートが検出しました。", "xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsTitle": "このアクションは {agentsCount} {agentsCount, plural, one {# エージェント} other {# エージェント}}に影響します", "xpack.ingestManager.deleteDatasource.confirmModal.cancelButtonLabel": "キャンセル", @@ -8393,11 +8372,6 @@ "xpack.ingestManager.deleteDatasource.successSingleNotificationTitle": "データソース「{id}」を削除しました", "xpack.ingestManager.disabledSecurityDescription": "Elastic Fleet を使用するには、Kibana と Elasticsearch でセキュリティを有効にする必要があります。", "xpack.ingestManager.disabledSecurityTitle": "セキュリティが有効ではありません", - "xpack.ingestManager.editConfig.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.editConfig.errorNotificationTitle": "エージェント構成を作成できません", - "xpack.ingestManager.editConfig.flyoutTitle": "構成を編集", - "xpack.ingestManager.editConfig.submitButtonLabel": "更新", - "xpack.ingestManager.editConfig.successNotificationTitle": "エージェント構成「{name}」を更新しました", "xpack.ingestManager.enrollmentApiKeyForm.namePlaceholder": "名前を選択", "xpack.ingestManager.enrollmentApiKeyList.createNewButton": "新規キーを作成", "xpack.ingestManager.enrollmentApiKeyList.useExistingsButton": "既存のキーを使用", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e373d05a7d851..323ddfda32d70 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8320,9 +8320,6 @@ "xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "命名空间", "xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "软件包", "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "流计数", - "xpack.ingestManager.configDetails.subTabs.datasouces": "数据源", - "xpack.ingestManager.configDetails.subTabs.settings": "设置", - "xpack.ingestManager.configDetails.subTabs.yamlFile": "YAML 文件", "xpack.ingestManager.configDetails.summary.datasources": "数据源", "xpack.ingestManager.configDetails.summary.lastUpdated": "最后更新时间", "xpack.ingestManager.configDetails.summary.revision": "修订", @@ -8332,10 +8329,6 @@ "xpack.ingestManager.configDetailsDatasources.createFirstButtonText": "创建数据源", "xpack.ingestManager.configDetailsDatasources.createFirstMessage": "此配置尚未有任何数据源。", "xpack.ingestManager.configDetailsDatasources.createFirstTitle": "创建您的首个数据源", - "xpack.ingestManager.configForm.descriptionFieldLabel": "描述", - "xpack.ingestManager.configForm.nameFieldLabel": "名称", - "xpack.ingestManager.configForm.nameRequiredErrorMessage": "配置名称必填", - "xpack.ingestManager.configForm.namespaceFieldLabel": "命名空间", "xpack.ingestManager.createAgentConfig.cancelButtonLabel": "取消", "xpack.ingestManager.createAgentConfig.errorNotificationTitle": "无法创建代理配置", "xpack.ingestManager.createAgentConfig.flyoutTitle": "创建代理配置", @@ -8367,20 +8360,6 @@ "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingPackagesTitle": "加载软件包时出错", "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingSelectedPackageTitle": "加载选定软件包时出错", "xpack.ingestManager.createDatasource.stepSelectPackage.filterPackagesInputPlaceholder": "搜索软件包", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# 个代理} other {# 个代理}}已分配{agentConfigsCount, plural, one {给此代理配置} other {给这些代理配置}}。将取消注册{agentsCount, plural, one {此代理} other {这些代理}}。", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.cancelButtonLabel": "取消", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmAndReassignButtonLabel": "删除{agentConfigsCount, plural, one {代理配置} other {代理配置}}并取消注册{agentsCount, plural, one {代理} other {代理}}", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmButtonLabel": "删除{agentConfigsCount, plural, one {代理配置} other {代理配置}}", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.deleteMultipleTitle": "删除{count, plural, one {此代理配置} other {# 个代理配置}}?", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingAgentsCountMessage": "正在检查受影响代理数量……", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingButtonLabel": "正在加载……", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.noAffectedAgentsMessage": "没有代理分配给{agentConfigsCount, plural, one {此代理配置} other {这些代理配置}}。", - "xpack.ingestManager.deleteAgentConfigs.failureMultipleNotificationTitle": "删除 {count} 个代理配置时出错", - "xpack.ingestManager.deleteAgentConfigs.failureSingleNotificationTitle": "删除代理配置“{id}”时出错", - "xpack.ingestManager.deleteAgentConfigs.fatalErrorNotificationTitle": "删除代理配置时出错", - "xpack.ingestManager.deleteAgentConfigs.successMultipleNotificationTitle": "已删除 {count} 个代理配置", - "xpack.ingestManager.deleteAgentConfigs.successSingleNotificationTitle": "已删除代理配置“{id}”", - "xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel": "取消", "xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsMessage": "Fleet 已检测到 {agentConfigName} 已由您的部分代理使用。", "xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsTitle": "此操作将影响 {agentsCount} 个 {agentsCount, plural, one {代理} other {代理}}。", "xpack.ingestManager.deleteDatasource.confirmModal.cancelButtonLabel": "取消", @@ -8396,11 +8375,6 @@ "xpack.ingestManager.deleteDatasource.successSingleNotificationTitle": "已删除数据源“{id}”", "xpack.ingestManager.disabledSecurityDescription": "必须在 Kibana 和 Elasticsearch 启用安全性,才能使用 Elastic Fleet。", "xpack.ingestManager.disabledSecurityTitle": "安全性未启用", - "xpack.ingestManager.editConfig.cancelButtonLabel": "取消", - "xpack.ingestManager.editConfig.errorNotificationTitle": "无法更新代理配置", - "xpack.ingestManager.editConfig.flyoutTitle": "编辑配置", - "xpack.ingestManager.editConfig.submitButtonLabel": "更新", - "xpack.ingestManager.editConfig.successNotificationTitle": "代理配置“{name}”已更新", "xpack.ingestManager.enrollmentApiKeyForm.namePlaceholder": "选择名称", "xpack.ingestManager.enrollmentApiKeyList.createNewButton": "创建新密钥", "xpack.ingestManager.enrollmentApiKeyList.useExistingsButton": "使用现有密钥", From b93427b7b65662ae082b06a9f9fb4d77c3acb81d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 30 Apr 2020 19:35:20 +0100 Subject: [PATCH 045/122] chore(NA): ignore server watch for md and test.tsx files (#64797) * chore(NA): avoid unnecessary server watches on md and test.tsx files * chore(NA): include debug log files --- src/cli/cluster/cluster_manager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index 32a23d74dbda4..f16ba61234a95 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -259,7 +259,9 @@ export class ClusterManager { const ignorePaths = [ /[\\\/](\..*|node_modules|bower_components|target|public|__[a-z0-9_]+__|coverage)([\\\/]|$)/, - /\.test\.(js|ts)$/, + /\.test\.(js|tsx?)$/, + /\.md$/, + /debug\.log$/, ...pluginInternalDirsIgnore, fromRoot('src/legacy/server/sass/__tmp__'), fromRoot('x-pack/legacy/plugins/reporting/.chromium'), From 5887c97d751fd639c4e24e5499b4ceed32fa4fcc Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 30 Apr 2020 20:36:50 +0200 Subject: [PATCH 046/122] [Lens] Allow user to drag and select a subset of the timeline in the chart (aka brush interaction) (#62636) * feat: brushing basic example for time histogram * test: added * refactor: simplify the structure * refactor: move to inline function * refactor * refactor * Always use time field from index pattern * types * use the meta.aggConfigParams for timefieldName * fix: test snapshot update * Update embeddable.tsx removing commented code * fix: moment remov * fix: corrections for adapting to timepicker on every timefield * fix: fix single bar condition * types Co-authored-by: Elastic Machine Co-authored-by: Wylie Conlon --- .../embeddable/embeddable.tsx | 4 +- .../public/{xy_visualization => }/services.ts | 4 +- .../__snapshots__/xy_expression.test.tsx.snap | 7 + .../lens/public/xy_visualization/index.ts | 2 +- .../public/xy_visualization/to_expression.ts | 2 +- .../xy_visualization/xy_expression.test.tsx | 174 +++++++++++++++++- .../public/xy_visualization/xy_expression.tsx | 85 +++++++-- 7 files changed, 253 insertions(+), 25 deletions(-) rename x-pack/plugins/lens/public/{xy_visualization => }/services.ts (71%) diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 0ef5f6d1a5470..6cd15e3c93e4e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -96,9 +96,7 @@ export class Embeddable extends AbstractEmbeddable { const operation = datasource.getOperationForColumnId(accessor); - if (operation && operation.label) { + if (operation?.label) { columnToLabel[accessor] = operation.label; } }); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index 8db00aba0e36d..dd2f32b63e34a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -27,6 +27,145 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; const executeTriggerActions = jest.fn(); +const dateHistogramData: LensMultiTable = { + type: 'lens_multitable', + tables: { + timeLayer: { + type: 'kibana_datatable', + rows: [ + { + xAccessorId: 1585758120000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Accessories", + yAccessorId: 1, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760700000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + { + xAccessorId: 1585761120000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + ], + columns: [ + { + id: 'xAccessorId', + name: 'order_date per minute', + meta: { + type: 'date_histogram', + indexPatternId: 'indexPatternId', + aggConfigParams: { + field: 'order_date', + timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: '1m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, + }, + formatHint: { id: 'date', params: { pattern: 'HH:mm' } }, + }, + { + id: 'splitAccessorId', + name: 'Top values of category.keyword', + meta: { + type: 'terms', + indexPatternId: 'indexPatternId', + aggConfigParams: { + field: 'category.keyword', + orderBy: 'yAccessorId', + order: 'desc', + size: 3, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + formatHint: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/jiy/app/kibana', + basePath: '/jiy', + }, + }, + }, + }, + { + id: 'yAccessorId', + name: 'Count of records', + meta: { + type: 'count', + indexPatternId: 'indexPatternId', + aggConfigParams: {}, + }, + formatHint: { id: 'number' }, + }, + ], + }, + }, + dateRange: { + fromDate: new Date('2020-04-01T16:14:16.246Z'), + toDate: new Date('2020-04-01T17:15:41.263Z'), + }, +}; + +const dateHistogramLayer: LayerArgs = { + layerId: 'timeLayer', + hide: false, + xAccessor: 'xAccessorId', + yScaleType: 'linear', + xScaleType: 'time', + isHistogram: true, + splitAccessor: 'splitAccessorId', + seriesType: 'bar_stacked', + accessors: ['yAccessorId'], +}; + const createSampleDatatableWithRows = (rows: KibanaDatatableRow[]): KibanaDatatable => ({ type: 'kibana_datatable', columns: [ @@ -284,7 +423,7 @@ describe('xy_expression', () => { Object { "max": 1546491600000, "min": 1546405200000, - "minInterval": 1728000, + "minInterval": undefined, } `); }); @@ -449,6 +588,39 @@ describe('xy_expression', () => { expect(component.find(Settings).prop('rotation')).toEqual(90); }); + test('onBrushEnd returns correct context data for date histogram data', () => { + const { args } = sampleArgs(); + + const wrapper = mountWithIntl( + + ); + + wrapper + .find(Settings) + .first() + .prop('onBrushEnd')!(1585757732783, 1585758880838); + + expect(executeTriggerActions).toHaveBeenCalledWith('SELECT_RANGE_TRIGGER', { + data: { + column: 0, + table: dateHistogramData.tables.timeLayer, + range: [1585757732783, 1585758880838], + }, + timeFieldName: 'order_date', + }); + }); + test('onElementClick returns correct context data', () => { const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null }; const series = { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index 85cf5753befd7..2cd9ae7acb203 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -29,14 +29,17 @@ import { import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { ValueClickTriggerContext } from '../../../../../src/plugins/embeddable/public'; +import { + ValueClickTriggerContext, + RangeSelectTriggerContext, +} from '../../../../../src/plugins/embeddable/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; import { LensMultiTable, FormatFactory } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } from './state_helpers'; +import { getExecuteTriggerActions } from '../services'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; -import { getExecuteTriggerActions } from './services'; import { parseInterval } from '../../../../../src/plugins/data/common'; type InferPropType = T extends React.FunctionComponent ? P : T; @@ -218,8 +221,32 @@ export function XYChart({ const xTitle = (xAxisColumn && xAxisColumn.name) || args.xTitle; function calculateMinInterval() { - // add minInterval only for single row value as it cannot be determined from dataset - if (data.dateRange && layers.every(layer => data.tables[layer.layerId].rows.length <= 1)) { + // check all the tables to see if all of the rows have the same timestamp + // that would mean that chart will draw a single bar + const isSingleTimestampInXDomain = () => { + const nonEmptyLayers = layers.filter( + layer => data.tables[layer.layerId].rows.length && layer.xAccessor + ); + + if (!nonEmptyLayers.length) { + return; + } + + const firstRowValue = + data.tables[nonEmptyLayers[0].layerId].rows[0][nonEmptyLayers[0].xAccessor!]; + for (const layer of nonEmptyLayers) { + if ( + layer.xAccessor && + data.tables[layer.layerId].rows.some(row => row[layer.xAccessor!] !== firstRowValue) + ) { + return false; + } + } + return true; + }; + + // add minInterval only for single point in domain + if (data.dateRange && isSingleTimestampInXDomain()) { if (xAxisColumn?.meta?.aggConfigParams?.interval !== 'auto') return parseInterval(xAxisColumn?.meta?.aggConfigParams?.interval)?.asMilliseconds(); @@ -231,14 +258,16 @@ export function XYChart({ return undefined; } - const xDomain = - data.dateRange && layers.every(l => l.xScaleType === 'time') - ? { - min: data.dateRange.fromDate.getTime(), - max: data.dateRange.toDate.getTime(), - minInterval: calculateMinInterval(), - } - : undefined; + const isTimeViz = data.dateRange && layers.every(l => l.xScaleType === 'time'); + + const xDomain = isTimeViz + ? { + min: data.dateRange?.fromDate.getTime(), + max: data.dateRange?.toDate.getTime(), + minInterval: calculateMinInterval(), + } + : undefined; + return ( { + // in the future we want to make it also for histogram + if (!xAxisColumn || !isTimeViz) { + return; + } + + const firstLayerWithData = + layers[layers.findIndex(layer => data.tables[layer.layerId].rows.length)]; + const table = data.tables[firstLayerWithData.layerId]; + + const xAxisColumnIndex = table.columns.findIndex( + el => el.id === firstLayerWithData.xAccessor + ); + const timeFieldName = table.columns[xAxisColumnIndex]?.meta?.aggConfigParams?.field; + + const context: RangeSelectTriggerContext = { + data: { + range: [min, max], + table, + column: xAxisColumnIndex, + }, + timeFieldName, + }; + executeTriggerActions(VIS_EVENT_TO_TRIGGER.brush, context); + }} onElementClick={([[geometry, series]]) => { // for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue const xySeries = series as XYChartSeriesIdentifier; @@ -284,10 +338,8 @@ export function XYChart({ }); } - const xAxisFieldName: string | undefined = table.columns.find( - col => col.id === layer.xAccessor - )?.meta?.aggConfigParams?.field; - + const xAxisFieldName = table.columns.find(el => el.id === layer.xAccessor)?.meta + ?.aggConfigParams?.field; const timeFieldName = xDomain && xAxisFieldName; const context: ValueClickTriggerContext = { @@ -301,7 +353,6 @@ export function XYChart({ }, timeFieldName, }; - executeTriggerActions(VIS_EVENT_TO_TRIGGER.filter, context); }} /> From c8b9bdd0ec479f3513223a79c5c63d2dbdcea989 Mon Sep 17 00:00:00 2001 From: spalger Date: Thu, 30 Apr 2020 11:48:41 -0700 Subject: [PATCH 047/122] skip flaky suite (#64473) --- .../apis/management/index_management/indices.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index d2d07eca475e7..9442beda3501d 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -34,7 +34,8 @@ export default function({ getService }) { clearCache, } = registerHelpers({ supertest }); - describe('indices', () => { + // FLAKY: https://github.com/elastic/kibana/issues/64473 + describe.skip('indices', () => { after(() => Promise.all([cleanUpEsResources()])); describe('clear cache', () => { From 0399f70050458b21f9a11d0bee39aa653a8bc97a Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 30 Apr 2020 11:52:11 -0700 Subject: [PATCH 048/122] [Metrics UI] Add Charts to Alert Conditions (#64384) * [Metrics UI] Add Charts to Alert Conditions - Reorganize files under public/alerting - Change Metrics Explorer API to force interval - Add charts to expression rows - Allow expression rows to be collapsable - Adding sum aggregation to Metrics Explorer for parity * Adding interval information to Metrics Eexplorer API * Moving data hook into the expression charts component * Revert "Adding interval information to Metrics Eexplorer API" This reverts commit f6e2fc11be9d239dac4518178ed880f167a74f5a. * Reducing the opacity for the threshold areas * Changing darkMode to use alertsContext.uiSettings --- .../index.ts => metrics_explorer.ts} | 2 + .../saved_objects/metrics_explorer_view.ts | 3 + .../components}/alert_dropdown.tsx | 0 .../components}/alert_flyout.tsx | 0 .../components}/expression.tsx | 261 ++------------- .../components/expression_chart.tsx | 302 ++++++++++++++++++ .../components/expression_row.tsx | 237 ++++++++++++++ .../components}/validation.tsx | 0 .../hooks/use_metrics_explorer_chart_data.ts | 59 ++++ .../metric_threshold/index.ts} | 10 +- .../lib/transform_metrics_explorer_data.ts | 31 ++ .../public/alerting/metric_threshold/types.ts | 51 +++ .../infra/public/pages/metrics/index.tsx | 2 +- .../components/waffle/node_context_menu.tsx | 2 +- .../components/aggregation.tsx | 3 + .../components/chart_context_menu.tsx | 2 +- .../components/helpers/create_metric_label.ts | 7 +- .../components/series_chart.tsx | 23 +- .../hooks/use_metrics_explorer_data.ts | 17 +- .../hooks/use_metrics_explorer_options.ts | 1 + x-pack/plugins/infra/public/plugin.ts | 4 +- .../lib/populate_series_with_tsvb_data.ts | 4 +- 22 files changed, 770 insertions(+), 251 deletions(-) rename x-pack/plugins/infra/common/http_api/{metrics_explorer/index.ts => metrics_explorer.ts} (98%) rename x-pack/plugins/infra/public/{components/alerting/metrics => alerting/metric_threshold/components}/alert_dropdown.tsx (100%) rename x-pack/plugins/infra/public/{components/alerting/metrics => alerting/metric_threshold/components}/alert_flyout.tsx (100%) rename x-pack/plugins/infra/public/{components/alerting/metrics => alerting/metric_threshold/components}/expression.tsx (59%) create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx rename x-pack/plugins/infra/public/{components/alerting/metrics => alerting/metric_threshold/components}/validation.tsx (100%) create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts rename x-pack/plugins/infra/public/{components/alerting/metrics/metric_threshold_alert_type.ts => alerting/metric_threshold/index.ts} (72%) create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/types.ts diff --git a/x-pack/plugins/infra/common/http_api/metrics_explorer/index.ts b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts similarity index 98% rename from x-pack/plugins/infra/common/http_api/metrics_explorer/index.ts rename to x-pack/plugins/infra/common/http_api/metrics_explorer.ts index 93655f931f45d..eb5b0bdbcfbc5 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_explorer/index.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts @@ -13,6 +13,7 @@ export const METRIC_EXPLORER_AGGREGATIONS = [ 'cardinality', 'rate', 'count', + 'sum', ] as const; type MetricExplorerAggregations = typeof METRIC_EXPLORER_AGGREGATIONS[number]; @@ -54,6 +55,7 @@ export const metricsExplorerRequestBodyOptionalFieldsRT = rt.partial({ afterKey: rt.union([rt.string, rt.null, rt.undefined]), limit: rt.union([rt.number, rt.null, rt.undefined]), filterQuery: rt.union([rt.string, rt.null, rt.undefined]), + forceInterval: rt.boolean, }); export const metricsExplorerRequestBodyRT = rt.intersection([ diff --git a/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts b/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts index 6eb08eabc15b5..add6ab0f132b5 100644 --- a/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts +++ b/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts @@ -35,6 +35,9 @@ export const metricsExplorerViewSavedObjectMappings: { }, options: { properties: { + forceInterval: { + type: 'boolean', + }, metrics: { type: 'nested', properties: { diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx rename to x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx rename to x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx similarity index 59% rename from x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx rename to x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index f2ac34ccb752f..352ac1927479e 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -6,9 +6,6 @@ import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react'; import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonIcon, EuiSpacer, EuiText, EuiFormRow, @@ -18,40 +15,28 @@ import { EuiIcon, EuiFieldSearch, } from '@elastic/eui'; -import { IFieldType } from 'src/plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { - MetricExpressionParams, Comparator, Aggregators, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../server/lib/alerting/metric_threshold/types'; -import { euiStyled } from '../../../../../observability/public'; import { - WhenExpression, - OfExpression, - ThresholdExpression, ForLastExpression, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../triggers_actions_ui/public/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { builtInComparators } from '../../../../../triggers_actions_ui/public/common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by'; import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; - -interface AlertContextMeta { - currentOptions?: Partial; - series?: MetricsExplorerSeries; -} +import { ExpressionRow } from './expression_row'; +import { AlertContextMeta, TimeUnit, MetricExpression } from '../types'; +import { ExpressionChart } from './expression_chart'; interface Props { errors: IErrorObject[]; @@ -67,11 +52,6 @@ interface Props { setAlertProperty(key: string, value: any): void; } -type TimeUnit = 's' | 'm' | 'h' | 'd'; -type MetricExpression = Omit & { - metric?: string; -}; - const defaultExpression = { aggType: Aggregators.AVERAGE, comparator: Comparator.GT, @@ -80,17 +60,6 @@ const defaultExpression = { timeUnit: 'm', } as MetricExpression; -const customComparators = { - ...builtInComparators, - [Comparator.OUTSIDE_RANGE]: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.outsideRangeLabel', { - defaultMessage: 'Is not between', - }), - value: Comparator.OUTSIDE_RANGE, - requiredValues: 2, - }, -}; - export const Expressions: React.FC = props => { const { setAlertParams, alertParams, errors, alertsContext } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ @@ -101,7 +70,6 @@ export const Expressions: React.FC = props => { }); const [timeSize, setTimeSize] = useState(1); const [timeUnit, setTimeUnit] = useState('m'); - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ createDerivedIndexPattern, ]); @@ -127,14 +95,14 @@ export const Expressions: React.FC = props => { ); const addExpression = useCallback(() => { - const exp = alertParams.criteria.slice(); + const exp = alertParams.criteria?.slice() || []; exp.push(defaultExpression); setAlertParams('criteria', exp); }, [setAlertParams, alertParams.criteria]); const removeExpression = useCallback( (id: number) => { - const exp = alertParams.criteria.slice(); + const exp = alertParams.criteria?.slice() || []; if (exp.length > 1) { exp.splice(id, 1); setAlertParams('criteria', exp); @@ -167,10 +135,11 @@ export const Expressions: React.FC = props => { const updateTimeSize = useCallback( (ts: number | undefined) => { - const criteria = alertParams.criteria.map(c => ({ - ...c, - timeSize: ts, - })); + const criteria = + alertParams.criteria?.map(c => ({ + ...c, + timeSize: ts, + })) || []; setTimeSize(ts || undefined); setAlertParams('criteria', criteria); }, @@ -179,10 +148,11 @@ export const Expressions: React.FC = props => { const updateTimeUnit = useCallback( (tu: string) => { - const criteria = alertParams.criteria.map(c => ({ - ...c, - timeUnit: tu, - })); + const criteria = + alertParams.criteria?.map(c => ({ + ...c, + timeUnit: tu, + })) || []; setTimeUnit(tu as TimeUnit); setAlertParams('criteria', criteria); }, @@ -250,7 +220,7 @@ export const Expressions: React.FC = props => { alertParams.criteria.map((e, idx) => { return ( 1} + canDelete={(alertParams.criteria && alertParams.criteria.length > 1) || false} fields={derivedIndexPattern.fields} remove={removeExpression} addExpression={addExpression} @@ -259,17 +229,28 @@ export const Expressions: React.FC = props => { setAlertParams={updateParams} errors={errors[idx] || emptyError} expression={e || {}} - /> + > + + ); })} - +
+ +
= props => { ); }; - -interface ExpressionRowProps { - fields: IFieldType[]; - expressionId: number; - expression: MetricExpression; - errors: IErrorObject; - canDelete: boolean; - addExpression(): void; - remove(id: number): void; - setAlertParams(id: number, params: MetricExpression): void; -} - -const StyledExpressionRow = euiStyled(EuiFlexGroup)` - display: flex; - flex-wrap: wrap; - margin: 0 -4px; -`; - -const StyledExpression = euiStyled.div` - padding: 0 4px; -`; - -export const ExpressionRow: React.FC = props => { - const { setAlertParams, expression, errors, expressionId, remove, fields, canDelete } = props; - const { - aggType = Aggregators.MAX, - metric, - comparator = Comparator.GT, - threshold = [], - } = expression; - - const updateAggType = useCallback( - (at: string) => { - setAlertParams(expressionId, { - ...expression, - aggType: at as MetricExpression['aggType'], - metric: at === 'count' ? undefined : expression.metric, - }); - }, - [expressionId, expression, setAlertParams] - ); - - const updateMetric = useCallback( - (m?: MetricExpression['metric']) => { - setAlertParams(expressionId, { ...expression, metric: m }); - }, - [expressionId, expression, setAlertParams] - ); - - const updateComparator = useCallback( - (c?: string) => { - setAlertParams(expressionId, { ...expression, comparator: c as Comparator }); - }, - [expressionId, expression, setAlertParams] - ); - - const updateThreshold = useCallback( - t => { - if (t.join() !== expression.threshold.join()) { - setAlertParams(expressionId, { ...expression, threshold: t }); - } - }, - [expressionId, expression, setAlertParams] - ); - - return ( - <> - - - - - - - {aggType !== 'count' && ( - - ({ - normalizedType: f.type, - name: f.name, - }))} - aggType={aggType} - errors={errors} - onChangeSelectedAggField={updateMetric} - /> - - )} - - - - - - {canDelete && ( - - remove(expressionId)} - /> - - )} - - - - ); -}; - -export const aggregationType: { [key: string]: any } = { - avg: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', { - defaultMessage: 'Average', - }), - fieldRequired: true, - validNormalizedTypes: ['number'], - value: Aggregators.AVERAGE, - }, - max: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.max', { - defaultMessage: 'Max', - }), - fieldRequired: true, - validNormalizedTypes: ['number', 'date'], - value: Aggregators.MAX, - }, - min: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.min', { - defaultMessage: 'Min', - }), - fieldRequired: true, - validNormalizedTypes: ['number', 'date'], - value: Aggregators.MIN, - }, - cardinality: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.cardinality', { - defaultMessage: 'Cardinality', - }), - fieldRequired: false, - value: Aggregators.CARDINALITY, - validNormalizedTypes: ['number'], - }, - rate: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.rate', { - defaultMessage: 'Rate', - }), - fieldRequired: false, - value: Aggregators.RATE, - validNormalizedTypes: ['number'], - }, - count: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.count', { - defaultMessage: 'Document count', - }), - fieldRequired: false, - value: Aggregators.COUNT, - validNormalizedTypes: ['number'], - }, -}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx new file mode 100644 index 0000000000000..a600d59865ccc --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -0,0 +1,302 @@ +/* + * 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, { useMemo, useCallback } from 'react'; +import { + Axis, + Chart, + niceTimeFormatter, + Position, + Settings, + TooltipValue, + RectAnnotation, +} from '@elastic/charts'; +import { first, last } from 'lodash'; +import moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { IIndexPattern } from 'src/plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +import { InfraSource } from '../../../../common/http_api/source_api'; +import { + Comparator, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/metric_threshold/types'; +import { MetricsExplorerColor, colorTransformer } from '../../../../common/color_palette'; +import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api'; +import { MetricExplorerSeriesChart } from '../../../pages/metrics/metrics_explorer/components/series_chart'; +import { MetricExpression, AlertContextMeta } from '../types'; +import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { getChartTheme } from '../../../pages/metrics/metrics_explorer/components/helpers/get_chart_theme'; +import { createFormatterForMetric } from '../../../pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric'; +import { calculateDomain } from '../../../pages/metrics/metrics_explorer/components/helpers/calculate_domain'; +import { useMetricsExplorerChartData } from '../hooks/use_metrics_explorer_chart_data'; + +interface Props { + context: AlertsContextValue; + expression: MetricExpression; + derivedIndexPattern: IIndexPattern; + source: InfraSource | null; + filterQuery?: string; + groupBy?: string; +} + +const tooltipProps = { + headerFormatter: (tooltipValue: TooltipValue) => + moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'), +}; + +const TIME_LABELS = { + s: i18n.translate('xpack.infra.metrics.alerts.timeLabels.seconds', { defaultMessage: 'seconds' }), + m: i18n.translate('xpack.infra.metrics.alerts.timeLabels.minutes', { defaultMessage: 'minutes' }), + h: i18n.translate('xpack.infra.metrics.alerts.timeLabels.hours', { defaultMessage: 'hours' }), + d: i18n.translate('xpack.infra.metrics.alerts.timeLabels.days', { defaultMessage: 'days' }), +}; + +export const ExpressionChart: React.FC = ({ + expression, + context, + derivedIndexPattern, + source, + filterQuery, + groupBy, +}) => { + const { loading, data } = useMetricsExplorerChartData( + expression, + context, + derivedIndexPattern, + source, + filterQuery, + groupBy + ); + + const metric = { + field: expression.metric, + aggregation: expression.aggType as MetricsExplorerAggregation, + color: MetricsExplorerColor.color0, + }; + const isDarkMode = context.uiSettings?.get('theme:darkMode') || false; + const dateFormatter = useMemo(() => { + const firstSeries = data ? first(data.series) : null; + return firstSeries && firstSeries.rows.length > 0 + ? niceTimeFormatter([first(firstSeries.rows).timestamp, last(firstSeries.rows).timestamp]) + : (value: number) => `${value}`; + }, [data]); + + const yAxisFormater = useCallback(createFormatterForMetric(metric), [expression]); + + if (loading || !data) { + return ( + + + + + + ); + } + + const thresholds = expression.threshold.slice().sort(); + + // Creating a custom series where the ID is changed to 0 + // so that we can get a proper domian + const firstSeries = first(data.series); + if (!firstSeries) { + return ( + + Oops, no chart data available + + ); + } + + const series = { + ...firstSeries, + rows: firstSeries.rows.map(row => { + const newRow: MetricsExplorerRow = { + timestamp: row.timestamp, + metric_0: row.metric_0 || null, + }; + thresholds.forEach((thresholdValue, index) => { + newRow[`metric_threshold_${index}`] = thresholdValue; + }); + return newRow; + }), + }; + + const firstTimestamp = first(firstSeries.rows).timestamp; + const lastTimestamp = last(firstSeries.rows).timestamp; + const dataDomain = calculateDomain(series, [metric], false); + const domain = { + max: Math.max(dataDomain.max, last(thresholds) || dataDomain.max) * 1.1, // add 10% headroom. + min: Math.min(dataDomain.min, first(thresholds) || dataDomain.min), + }; + + if (domain.min === first(expression.threshold)) { + domain.min = domain.min * 0.9; + } + + const isAbove = [Comparator.GT, Comparator.GT_OR_EQ].includes(expression.comparator); + const opacity = 0.3; + const timeLabel = TIME_LABELS[expression.timeUnit]; + + return ( + <> + + + + {thresholds.length ? ( + `threshold_${i}`)} + series={series} + stack={false} + opacity={opacity} + /> + ) : null} + {thresholds.length && expression.comparator === Comparator.OUTSIDE_RANGE ? ( + <> + `threshold_${i}`)} + series={series} + stack={false} + opacity={opacity} + /> + + + + ) : null} + {isAbove ? ( + + ) : null} + + + + + +
+ {series.id !== 'ALL' ? ( + + + + ) : ( + + + + )} +
+ + ); +}; + +const EmptyContainer: React.FC = ({ children }) => ( +
+ {children} +
+); + +const ChartContainer: React.FC = ({ children }) => ( +
+ {children} +
+); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx new file mode 100644 index 0000000000000..8801df380b48d --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx @@ -0,0 +1,237 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiSpacer } from '@elastic/eui'; +import { IFieldType } from 'src/plugins/data/public'; +import { + WhenExpression, + OfExpression, + ThresholdExpression, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/common'; +import { euiStyled } from '../../../../../observability/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +import { MetricExpression, AGGREGATION_TYPES } from '../types'; +import { + Comparator, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/metric_threshold/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { builtInComparators } from '../../../../../triggers_actions_ui/public/common/constants'; + +const customComparators = { + ...builtInComparators, + [Comparator.OUTSIDE_RANGE]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.outsideRangeLabel', { + defaultMessage: 'Is not between', + }), + value: Comparator.OUTSIDE_RANGE, + requiredValues: 2, + }, +}; + +interface ExpressionRowProps { + fields: IFieldType[]; + expressionId: number; + expression: MetricExpression; + errors: IErrorObject; + canDelete: boolean; + addExpression(): void; + remove(id: number): void; + setAlertParams(id: number, params: MetricExpression): void; +} + +const StyledExpressionRow = euiStyled(EuiFlexGroup)` + display: flex; + flex-wrap: wrap; + margin: 0 -4px; +`; + +const StyledExpression = euiStyled.div` + padding: 0 4px; +`; + +export const ExpressionRow: React.FC = props => { + const [isExpanded, setRowState] = useState(true); + const toggleRowState = useCallback(() => setRowState(!isExpanded), [isExpanded]); + const { + children, + setAlertParams, + expression, + errors, + expressionId, + remove, + fields, + canDelete, + } = props; + const { + aggType = AGGREGATION_TYPES.MAX, + metric, + comparator = Comparator.GT, + threshold = [], + } = expression; + + const updateAggType = useCallback( + (at: string) => { + setAlertParams(expressionId, { + ...expression, + aggType: at as MetricExpression['aggType'], + metric: at === 'count' ? undefined : expression.metric, + }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateMetric = useCallback( + (m?: MetricExpression['metric']) => { + setAlertParams(expressionId, { ...expression, metric: m }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateComparator = useCallback( + (c?: string) => { + setAlertParams(expressionId, { ...expression, comparator: c as Comparator }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateThreshold = useCallback( + t => { + if (t.join() !== expression.threshold.join()) { + setAlertParams(expressionId, { ...expression, threshold: t }); + } + }, + [expressionId, expression, setAlertParams] + ); + + return ( + <> + + + + + + + + + + {aggType !== 'count' && ( + + ({ + normalizedType: f.type, + name: f.name, + }))} + aggType={aggType} + errors={errors} + onChangeSelectedAggField={updateMetric} + /> + + )} + + + + + + {canDelete && ( + + remove(expressionId)} + /> + + )} + + {isExpanded ?
{children}
: null} + + + ); +}; + +export const aggregationType: { [key: string]: any } = { + avg: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', { + defaultMessage: 'Average', + }), + fieldRequired: true, + validNormalizedTypes: ['number'], + value: AGGREGATION_TYPES.AVERAGE, + }, + max: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.max', { + defaultMessage: 'Max', + }), + fieldRequired: true, + validNormalizedTypes: ['number', 'date'], + value: AGGREGATION_TYPES.MAX, + }, + min: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.min', { + defaultMessage: 'Min', + }), + fieldRequired: true, + validNormalizedTypes: ['number', 'date'], + value: AGGREGATION_TYPES.MIN, + }, + cardinality: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.cardinality', { + defaultMessage: 'Cardinality', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.CARDINALITY, + validNormalizedTypes: ['number'], + }, + rate: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.rate', { + defaultMessage: 'Rate', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.RATE, + validNormalizedTypes: ['number'], + }, + count: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.count', { + defaultMessage: 'Document count', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.COUNT, + validNormalizedTypes: ['number'], + }, + sum: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.sum', { + defaultMessage: 'Sum', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.SUM, + validNormalizedTypes: ['number'], + }, +}; diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx rename to x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts new file mode 100644 index 0000000000000..67f66bf742f43 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -0,0 +1,59 @@ +/* + * 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 { IIndexPattern } from 'src/plugins/data/public'; +import { useMemo } from 'react'; +import { InfraSource } from '../../../../common/http_api/source_api'; +import { AlertContextMeta, MetricExpression } from '../types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data'; + +export const useMetricsExplorerChartData = ( + expression: MetricExpression, + context: AlertsContextValue, + derivedIndexPattern: IIndexPattern, + source: InfraSource | null, + filterQuery?: string, + groupBy?: string +) => { + const { timeSize, timeUnit } = expression || { timeSize: 1, timeUnit: 'm' }; + const options: MetricsExplorerOptions = useMemo( + () => ({ + limit: 1, + forceInterval: true, + groupBy, + filterQuery, + metrics: [ + { + field: expression.metric, + aggregation: expression.aggType, + }, + ], + aggregation: expression.aggType || 'avg', + }), + [expression.aggType, expression.metric, filterQuery, groupBy] + ); + const timerange = useMemo( + () => ({ + interval: `>=${timeSize || 1}${timeUnit}`, + from: `now-${(timeSize || 1) * 20}${timeUnit}`, + to: 'now', + }), + [timeSize, timeUnit] + ); + + return useMetricsExplorerData( + options, + source?.configuration, + derivedIndexPattern, + timerange, + null, + null, + context.http.fetch + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/metric_threshold_alert_type.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts similarity index 72% rename from x-pack/plugins/infra/public/components/alerting/metrics/metric_threshold_alert_type.ts rename to x-pack/plugins/infra/public/alerting/metric_threshold/index.ts index 162102ebd86a8..91b9bafad5011 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts @@ -5,13 +5,13 @@ */ import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types'; -import { Expressions } from './expression'; -import { validateMetricThreshold } from './validation'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { Expressions } from './components/expression'; +import { validateMetricThreshold } from './components/validation'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../server/lib/alerting/metric_threshold/types'; -export function getAlertType(): AlertTypeModel { +export function createMetricThresholdAlertType(): AlertTypeModel { return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, name: i18n.translate('xpack.infra.metrics.alertFlyout.alertName', { diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts new file mode 100644 index 0000000000000..0e631b1e333d7 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.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. + */ + +import { first } from 'lodash'; +import { MetricsExplorerResponse } from '../../../../common/http_api/metrics_explorer'; +import { MetricThresholdAlertParams, ExpressionChartSeries } from '../types'; + +export const transformMetricsExplorerData = ( + params: MetricThresholdAlertParams, + data: MetricsExplorerResponse | null +) => { + const { criteria } = params; + if (criteria && data) { + const firstSeries = first(data.series); + const series = firstSeries.rows.reduce((acc, row) => { + const { timestamp } = row; + criteria.forEach((item, index) => { + if (!acc[index]) { + acc[index] = []; + } + const value = (row[`metric_${index}`] as number) || 0; + acc[index].push({ timestamp, value }); + }); + return acc; + }, [] as ExpressionChartSeries); + return { id: firstSeries.id, series }; + } +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts new file mode 100644 index 0000000000000..af3baf191bed2 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -0,0 +1,51 @@ +/* + * 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 { + MetricExpressionParams, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../server/lib/alerting/metric_threshold/types'; +import { MetricsExplorerOptions } from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer'; + +export interface AlertContextMeta { + currentOptions?: Partial; + series?: MetricsExplorerSeries; +} + +export type TimeUnit = 's' | 'm' | 'h' | 'd'; +export type MetricExpression = Omit & { + metric?: string; +}; + +export enum AGGREGATION_TYPES { + COUNT = 'count', + AVERAGE = 'avg', + SUM = 'sum', + MIN = 'min', + MAX = 'max', + RATE = 'rate', + CARDINALITY = 'cardinality', +} + +export interface MetricThresholdAlertParams { + criteria?: MetricExpression[]; + groupBy?: string; + filterQuery?: string; + sourceId?: string; +} + +export interface ExpressionChartRow { + timestamp: number; + value: number; +} + +export type ExpressionChartSeries = ExpressionChartRow[][]; + +export interface ExpressionChartData { + id: string; + series: ExpressionChartSeries; +} diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index cc88dd9e0d0f8..5dc9802fefd25 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -28,7 +28,7 @@ import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options'; import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time'; import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters'; -import { AlertDropdown } from '../../components/alerting/metrics/alert_dropdown'; +import { AlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; export const InfrastructurePage = ({ match }: RouteComponentProps) => { const uiCapabilities = useKibana().services.application?.capabilities; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx index 275635a33ec26..c528aa885346e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo, useState } from 'react'; -import { AlertFlyout } from '../../../../../components/alerting/metrics/alert_flyout'; +import { AlertFlyout } from '../../../../../alerting/metric_threshold/components/alert_flyout'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; import { getNodeDetailUrl, getNodeLogsUrl } from '../../../../link_to'; import { createUptimeLink } from '../../lib/create_uptime_link'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx index 0c0f7b33b3a4a..5a84d204b3b25 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx @@ -26,6 +26,9 @@ export const MetricsExplorerAggregationPicker = ({ options, onChange }: Props) = ['avg']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.avg', { defaultMessage: 'Average', }), + ['sum']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.sum', { + defaultMessage: 'Sum', + }), ['max']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.max', { defaultMessage: 'Max', }), diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx index 31086a21ca13f..e13c9dcc06984 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx @@ -14,7 +14,7 @@ import { } from '@elastic/eui'; import DateMath from '@elastic/datemath'; import { Capabilities } from 'src/core/public'; -import { AlertFlyout } from '../../../../components/alerting/metrics/alert_flyout'; +import { AlertFlyout } from '../../../../alerting/metric_threshold/components/alert_flyout'; import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts index 1607302a6259a..6343f2848054b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerOptionsMetric } from '../../hooks/use_metrics_explorer_options'; -export const createMetricLabel = (metric: MetricsExplorerMetric) => { +export const createMetricLabel = (metric: MetricsExplorerOptionsMetric) => { + if (metric.label) { + return metric.label; + } return `${metric.aggregation}(${metric.field || ''})`; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx index ad7ce83539526..3b84fcbc34836 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx @@ -21,12 +21,15 @@ import { MetricsExplorerChartType, } from '../hooks/use_metrics_explorer_options'; +type NumberOrString = string | number; + interface Props { metric: MetricsExplorerOptionsMetric; - id: string | number; + id: NumberOrString | NumberOrString[]; series: MetricsExplorerSeries; type: MetricsExplorerChartType; stack: boolean; + opacity?: number; } export const MetricExplorerSeriesChart = (props: Props) => { @@ -36,13 +39,17 @@ export const MetricExplorerSeriesChart = (props: Props) => { return ; }; -export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack }: Props) => { +export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack, opacity }: Props) => { const color = (metric.color && colorTransformer(metric.color)) || colorTransformer(MetricsExplorerColor.color0); - const yAccessor = `metric_${id}`; - const chartId = `series-${series.id}-${yAccessor}`; + const yAccessors = Array.isArray(id) + ? id.map(i => `metric_${i}`).slice(id.length - 1, id.length) + : [`metric_${id}`]; + const y0Accessors = + Array.isArray(id) && id.length > 1 ? id.map(i => `metric_${i}`).slice(0, 1) : undefined; + const chartId = `series-${series.id}-${yAccessors.join('-')}`; const seriesAreaStyle: RecursivePartial = { line: { @@ -50,19 +57,21 @@ export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack }: Pr visible: true, }, area: { - opacity: 0.5, + opacity: opacity || 0.5, visible: type === MetricsExplorerChartType.area, }, }; + return ( (null); const [loading, setLoading] = useState(true); const [data, setData] = useState(null); @@ -46,13 +49,17 @@ export function useMetricsExplorerData( if (!from || !to) { throw new Error('Unalble to parse timerange'); } - if (!fetch) { + if (!fetchFn) { throw new Error('HTTP service is unavailable'); } + if (!source) { + throw new Error('Source is unavailable'); + } const response = decodeOrThrow(metricsExplorerResponseRT)( - await fetch('/api/infra/metrics_explorer', { + await fetchFn('/api/infra/metrics_explorer', { method: 'POST', body: JSON.stringify({ + forceInterval: options.forceInterval, metrics: options.aggregation === 'count' ? [{ aggregation: 'count' }] diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts index 9d124a6af8012..1b3e809fde61f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts @@ -40,6 +40,7 @@ export interface MetricsExplorerOptions { groupBy?: string; filterQuery?: string; aggregation: MetricsExplorerAggregation; + forceInterval?: boolean; } export interface MetricsExplorerTimeOptions { diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 40366b2a54f24..8cdfc4f381f43 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -21,8 +21,8 @@ import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/p import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; -import { getAlertType as getMetricsAlertType } from './components/alerting/metrics/metric_threshold_alert_type'; import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type'; +import { createMetricThresholdAlertType } from './alerting/metric_threshold'; export type ClientSetup = void; export type ClientStart = void; @@ -53,8 +53,8 @@ export class Plugin setup(core: CoreSetup, pluginsSetup: ClientPluginsSetup) { registerFeatures(pluginsSetup.home); - pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getMetricsAlertType()); pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getLogsAlertType()); + pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(createMetricThresholdAlertType()); core.application.register({ id: 'logs', diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts index 3517800ea0dd1..e735a26d96a91 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts @@ -72,7 +72,9 @@ export const populateSeriesWithTSVBData = ( ); if (calculatedInterval) { - model.interval = `>=${calculatedInterval}s`; + model.interval = options.forceInterval + ? options.timerange.interval + : `>=${calculatedInterval}s`; } // Get TSVB results using the model, timerange and filters From b8e0730d0cbd09346af042f70e1f7c02d933d9d3 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 30 Apr 2020 14:48:37 -0500 Subject: [PATCH 049/122] [SIEM] NP Plugin dependency cleanup (#64842) * Remove static src dependencies from kibana.json We are only importing static code from these plugins, and not consuming their plugin contracts. For this reason, we can safely remove them from kibana.json as that imported code will always be included in our own bundle. * Make usageCollection an optional dependency If it's not enabled, we simply use a noop for our tracker call. * Remove unused plugin contracts These are only needed when we're actually using them in our codebase. For request handler contexts, we only need our kibana.json declaration. * Remove unnecessary try/catch With the addition of the null coalescing, the only chance for an error is in usageCollection. However, if the usageCollection contract changes, we should get a type error long before we see a runtime error. * Improve typings of our Plugin classes * passes missing generic arguments to public plugin interface * passes missing generic arguments to both plugins' CoreSetup types * Don't re-export core types Instead, import them from core as needed. --- x-pack/plugins/siem/kibana.json | 12 ++++++++---- .../plugins/siem/public/lib/telemetry/index.ts | 8 +++----- x-pack/plugins/siem/public/plugin.tsx | 6 +++--- x-pack/plugins/siem/server/lib/compose/kibana.ts | 3 ++- .../lib/framework/kibana_framework_adapter.ts | 3 ++- x-pack/plugins/siem/server/plugin.ts | 16 ++++------------ 6 files changed, 22 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/siem/kibana.json b/x-pack/plugins/siem/kibana.json index e0ff82eb10eb2..39e50838c1c97 100644 --- a/x-pack/plugins/siem/kibana.json +++ b/x-pack/plugins/siem/kibana.json @@ -8,18 +8,22 @@ "alerting", "data", "embeddable", - "esUiShared", "features", "home", "inspector", - "kibanaUtils", "licensing", "maps", "triggers_actions_ui", - "uiActions", + "uiActions" + ], + "optionalPlugins": [ + "encryptedSavedObjects", + "ml", + "newsfeed", + "security", + "spaces", "usageCollection" ], - "optionalPlugins": ["encryptedSavedObjects", "ml", "newsfeed", "security", "spaces"], "server": true, "ui": true } diff --git a/x-pack/plugins/siem/public/lib/telemetry/index.ts b/x-pack/plugins/siem/public/lib/telemetry/index.ts index 856b7783a5367..37d181e9b8ad7 100644 --- a/x-pack/plugins/siem/public/lib/telemetry/index.ts +++ b/x-pack/plugins/siem/public/lib/telemetry/index.ts @@ -13,6 +13,8 @@ export { METRIC_TYPE }; type TrackFn = (type: UiStatsMetricType, event: string | string[], count?: number) => void; +const noop = () => {}; + let _track: TrackFn; export const track: TrackFn = (type, event, count) => { @@ -24,11 +26,7 @@ export const track: TrackFn = (type, event, count) => { }; export const initTelemetry = (usageCollection: SetupPlugins['usageCollection'], appId: string) => { - try { - _track = usageCollection.reportUiStats.bind(null, appId); - } catch (error) { - // ignore failed setup here, as we'll just have an inert tracker - } + _track = usageCollection?.reportUiStats?.bind(null, appId) ?? noop; }; export enum TELEMETRY_EVENT { diff --git a/x-pack/plugins/siem/public/plugin.tsx b/x-pack/plugins/siem/public/plugin.tsx index e54c96364ba6b..f9c44bd341fac 100644 --- a/x-pack/plugins/siem/public/plugin.tsx +++ b/x-pack/plugins/siem/public/plugin.tsx @@ -37,7 +37,7 @@ export interface SetupPlugins { home: HomePublicPluginSetup; security: SecurityPluginSetup; triggers_actions_ui: TriggersActionsSetup; - usageCollection: UsageCollectionSetup; + usageCollection?: UsageCollectionSetup; } export interface StartPlugins { @@ -59,14 +59,14 @@ export interface PluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginStart {} -export class Plugin implements IPlugin { +export class Plugin implements IPlugin { private kibanaVersion: string; constructor(initializerContext: PluginInitializerContext) { this.kibanaVersion = initializerContext.env.packageInfo.version; } - public setup(core: CoreSetup, plugins: SetupPlugins) { + public setup(core: CoreSetup, plugins: SetupPlugins) { initTelemetry(plugins.usageCollection, APP_ID); plugins.home.featureCatalogue.register({ diff --git a/x-pack/plugins/siem/server/lib/compose/kibana.ts b/x-pack/plugins/siem/server/lib/compose/kibana.ts index 4a595032e43eb..8bc90bed25168 100644 --- a/x-pack/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/plugins/siem/server/lib/compose/kibana.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, SetupPlugins } from '../../plugin'; +import { CoreSetup } from '../../../../../../src/core/server'; +import { SetupPlugins } from '../../plugin'; import { Authentications } from '../authentications'; import { ElasticsearchAuthenticationAdapter } from '../authentications/elasticsearch_adapter'; diff --git a/x-pack/plugins/siem/server/lib/framework/kibana_framework_adapter.ts b/x-pack/plugins/siem/server/lib/framework/kibana_framework_adapter.ts index 762416149c0fb..1e99c40ef5727 100644 --- a/x-pack/plugins/siem/server/lib/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/siem/server/lib/framework/kibana_framework_adapter.ts @@ -9,6 +9,7 @@ import { GraphQLSchema } from 'graphql'; import { runHttpQuery } from 'apollo-server-core'; import { schema as configSchema } from '@kbn/config-schema'; import { + CoreSetup, IRouter, KibanaResponseFactory, RequestHandlerContext, @@ -16,7 +17,7 @@ import { } from '../../../../../../src/core/server'; import { IndexPatternsFetcher } from '../../../../../../src/plugins/data/server'; import { AuthenticatedUser } from '../../../../security/common/model'; -import { CoreSetup, SetupPlugins } from '../../plugin'; +import { SetupPlugins } from '../../plugin'; import { FrameworkAdapter, diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index dc484b75ab531..3ef4b39bd0979 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -15,16 +15,12 @@ import { PluginInitializerContext, Logger, } from '../../../../src/core/server'; -import { - PluginStartContract as AlertingStart, - PluginSetupContract as AlertingSetup, -} from '../../alerting/server'; +import { PluginSetupContract as AlertingSetup } from '../../alerting/server'; import { SecurityPluginSetup as SecuritySetup } from '../../security/server'; import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { MlPluginSetup as MlSetup } from '../../ml/server'; import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../encrypted_saved_objects/server'; import { SpacesPluginSetup as SpacesSetup } from '../../spaces/server'; -import { PluginStartContract as ActionsStart } from '../../actions/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; @@ -40,8 +36,6 @@ import { createConfig$, ConfigType } from './config'; import { initUiSettings } from './ui_settings'; import { APP_ID, APP_ICON } from '../common/constants'; -export { CoreSetup, CoreStart }; - export interface SetupPlugins { alerting: AlertingSetup; encryptedSavedObjects?: EncryptedSavedObjectsSetup; @@ -52,10 +46,8 @@ export interface SetupPlugins { ml?: MlSetup; } -export interface StartPlugins { - actions: ActionsStart; - alerting: AlertingStart; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface StartPlugins {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginSetup {} @@ -77,7 +69,7 @@ export class Plugin implements IPlugin, plugins: SetupPlugins) { this.logger.debug('plugin setup'); if (hasListsFeature()) { From 6e3791ea12c4283561e1cdd5284c1589274b2417 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 30 Apr 2020 15:52:14 -0400 Subject: [PATCH 050/122] Ingest Node Pipelines UI (#62321) --- .github/CODEOWNERS | 1 + x-pack/.i18nrc.json | 1 + x-pack/plugins/ingest_pipelines/README.md | 24 ++ .../ingest_pipelines/common/constants.ts | 18 + .../ingest_pipelines/common/lib/index.ts | 7 + .../common/lib/pipeline_serialization.test.ts | 68 ++++ .../common/lib/pipeline_serialization.ts | 20 ++ .../plugins/ingest_pipelines/common/types.ts | 28 ++ x-pack/plugins/ingest_pipelines/kibana.json | 17 + .../public/application/app.tsx | 101 ++++++ .../public/application/components/index.ts | 7 + .../components/pipeline_form/index.ts | 7 + .../pipeline_form/pipeline_form.tsx | 166 +++++++++ .../pipeline_form/pipeline_form_error.tsx | 34 ++ .../pipeline_form/pipeline_form_fields.tsx | 223 ++++++++++++ .../pipeline_form/pipeline_form_provider.tsx | 18 + .../pipeline_request_flyout/index.ts | 7 + .../pipeline_request_flyout.tsx | 89 +++++ .../pipeline_request_flyout_provider.tsx | 29 ++ .../pipeline_test_flyout/index.ts | 7 + .../pipeline_test_flyout.tsx | 203 +++++++++++ .../pipeline_test_flyout_provider.tsx | 39 +++ .../pipeline_test_flyout/tabs/index.ts | 11 + .../tabs/pipeline_test_tabs.tsx | 60 ++++ .../pipeline_test_flyout/tabs/schema.tsx | 87 +++++ .../tabs/tab_documents.tsx | 152 ++++++++ .../pipeline_test_flyout/tabs/tab_output.tsx | 106 ++++++ .../components/pipeline_form/schema.tsx | 147 ++++++++ .../pipeline_form/test_config_context.tsx | 57 +++ .../public/application/constants/index.ts | 14 + .../public/application/index.tsx | 55 +++ .../public/application/lib/index.ts | 7 + .../public/application/lib/utils.test.ts | 37 ++ .../public/application/lib/utils.ts | 25 ++ .../application/mount_management_section.ts | 35 ++ .../public/application/sections/index.ts | 13 + .../sections/pipelines_clone/index.ts | 7 + .../pipelines_clone/pipelines_clone.tsx | 59 ++++ .../sections/pipelines_create/index.ts | 7 + .../pipelines_create/pipelines_create.tsx | 109 ++++++ .../sections/pipelines_edit/index.ts | 7 + .../pipelines_edit/pipelines_edit.tsx | 151 ++++++++ .../sections/pipelines_list/delete_modal.tsx | 125 +++++++ .../pipelines_list/details_flyout.tsx | 210 +++++++++++ .../pipelines_list/details_json_block.tsx | 31 ++ .../sections/pipelines_list/empty_list.tsx | 29 ++ .../sections/pipelines_list/index.ts | 7 + .../sections/pipelines_list/main.tsx | 207 +++++++++++ .../pipelines_list/not_found_flyout.tsx | 42 +++ .../sections/pipelines_list/table.tsx | 148 ++++++++ .../public/application/services/api.ts | 125 +++++++ .../application/services/breadcrumbs.ts | 71 ++++ .../application/services/documentation.ts | 39 +++ .../public/application/services/index.ts | 13 + .../public/application/services/ui_metric.ts | 32 ++ .../plugins/ingest_pipelines/public/index.ts | 11 + .../plugins/ingest_pipelines/public/plugin.ts | 39 +++ .../ingest_pipelines/public/shared_imports.ts | 54 +++ .../plugins/ingest_pipelines/public/types.ts | 13 + .../plugins/ingest_pipelines/server/index.ts | 12 + .../ingest_pipelines/server/lib/index.ts | 7 + .../server/lib/is_es_error.ts | 13 + .../plugins/ingest_pipelines/server/plugin.ts | 59 ++++ .../server/routes/api/create.ts | 83 +++++ .../server/routes/api/delete.ts | 49 +++ .../ingest_pipelines/server/routes/api/get.ts | 83 +++++ .../server/routes/api/index.ts | 17 + .../server/routes/api/privileges.ts | 62 ++++ .../server/routes/api/simulate.ts | 61 ++++ .../server/routes/api/update.ts | 67 ++++ .../ingest_pipelines/server/routes/index.ts | 27 ++ .../ingest_pipelines/server/services/index.ts | 7 + .../server/services/license.ts | 82 +++++ .../plugins/ingest_pipelines/server/types.ts | 22 ++ .../api_integration/apis/management/index.js | 1 + .../apis/management/ingest_pipelines/index.ts | 12 + .../ingest_pipelines/ingest_pipelines.ts | 330 ++++++++++++++++++ .../ingest_pipelines/lib/elasticsearch.ts | 39 +++ .../management/ingest_pipelines/lib/index.ts | 7 + .../functional/apps/ingest_pipelines/index.ts | 14 + .../apps/ingest_pipelines/ingest_pipelines.ts | 27 ++ x-pack/test/functional/config.js | 5 + x-pack/test/functional/page_objects/index.ts | 2 + .../page_objects/ingest_pipelines_page.ts | 17 + 84 files changed, 4561 insertions(+) create mode 100644 x-pack/plugins/ingest_pipelines/README.md create mode 100644 x-pack/plugins/ingest_pipelines/common/constants.ts create mode 100644 x-pack/plugins/ingest_pipelines/common/lib/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts create mode 100644 x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts create mode 100644 x-pack/plugins/ingest_pipelines/common/types.ts create mode 100644 x-pack/plugins/ingest_pipelines/kibana.json create mode 100644 x-pack/plugins/ingest_pipelines/public/application/app.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_output.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/test_config_context.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/constants/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/index.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/lib/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/lib/utils.test.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/lib/utils.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/services/api.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/services/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/services/ui_metric.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/plugin.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/shared_imports.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/types.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/lib/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/lib/is_es_error.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/plugin.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/create.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/get.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/update.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/services/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/services/license.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/types.ts create mode 100644 x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts create mode 100644 x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts create mode 100644 x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts create mode 100644 x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts create mode 100644 x-pack/test/functional/apps/ingest_pipelines/index.ts create mode 100644 x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts create mode 100644 x-pack/test/functional/page_objects/ingest_pipelines_page.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a97400ee09c0e..c17538849660e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -203,6 +203,7 @@ /x-pack/plugins/snapshot_restore/ @elastic/es-ui /x-pack/plugins/upgrade_assistant/ @elastic/es-ui /x-pack/plugins/watcher/ @elastic/es-ui +/x-pack/plugins/ingest_pipelines/ @elastic/es-ui # Endpoint /x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 4acb170d12574..9f43bf8da0601 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -21,6 +21,7 @@ "xpack.indexLifecycleMgmt": "plugins/index_lifecycle_management", "xpack.infra": "plugins/infra", "xpack.ingestManager": "plugins/ingest_manager", + "xpack.ingestPipelines": "plugins/ingest_pipelines", "xpack.lens": "plugins/lens", "xpack.licenseMgmt": "plugins/license_management", "xpack.licensing": "plugins/licensing", diff --git a/x-pack/plugins/ingest_pipelines/README.md b/x-pack/plugins/ingest_pipelines/README.md new file mode 100644 index 0000000000000..a469511bdbbd2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/README.md @@ -0,0 +1,24 @@ +# Ingest Node Pipelines UI + +## Summary +The `ingest_pipelines` plugin provides Kibana support for [Elasticsearch's ingest nodes](https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest.html). Please refer to the Elasticsearch documentation for more details. + +This plugin allows Kibana to create, edit, clone and delete ingest node pipelines. It also provides support to simulate a pipeline. + +It requires a Basic license and the following cluster privileges: `manage_pipeline` and `cluster:monitor/nodes/info`. + +--- + +## Development + +A new app called Ingest Node Pipelines is registered in the Management section and follows a typical CRUD UI pattern. The client-side portion of this app lives in [public/application](public/application) and uses endpoints registered in [server/routes/api](server/routes/api). + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions on setting up your development environment. + +### Test coverage + +The app has the following test coverage: + +- Complete API integration tests +- Smoke-level functional test +- Client-integration tests diff --git a/x-pack/plugins/ingest_pipelines/common/constants.ts b/x-pack/plugins/ingest_pipelines/common/constants.ts new file mode 100644 index 0000000000000..edf681c276a84 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/constants.ts @@ -0,0 +1,18 @@ +/* + * 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 { LicenseType } from '../../licensing/common/types'; + +const basicLicense: LicenseType = 'basic'; + +export const PLUGIN_ID = 'ingest_pipelines'; + +export const PLUGIN_MIN_LICENSE_TYPE = basicLicense; + +export const BASE_PATH = '/management/elasticsearch/ingest_pipelines'; + +export const API_BASE_PATH = '/api/ingest_pipelines'; + +export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_pipeline', 'cluster:monitor/nodes/info']; diff --git a/x-pack/plugins/ingest_pipelines/common/lib/index.ts b/x-pack/plugins/ingest_pipelines/common/lib/index.ts new file mode 100644 index 0000000000000..a976f66bc7c40 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { deserializePipelines } from './pipeline_serialization'; diff --git a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts new file mode 100644 index 0000000000000..65d6b6e30497f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts @@ -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 { deserializePipelines } from './pipeline_serialization'; + +describe('pipeline_serialization', () => { + describe('deserializePipelines()', () => { + it('should deserialize pipelines', () => { + expect( + deserializePipelines({ + pipeline1: { + description: 'pipeline 1 description', + version: 1, + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + on_failure: [ + { + set: { + field: 'error.message', + value: '{{ failure_message }}', + }, + }, + ], + }, + pipeline2: { + description: 'pipeline2 description', + version: 1, + processors: [], + }, + }) + ).toEqual([ + { + name: 'pipeline1', + description: 'pipeline 1 description', + version: 1, + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + on_failure: [ + { + set: { + field: 'error.message', + value: '{{ failure_message }}', + }, + }, + ], + }, + { + name: 'pipeline2', + description: 'pipeline2 description', + version: 1, + processors: [], + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts new file mode 100644 index 0000000000000..572f655076015 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts @@ -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 { PipelinesByName, Pipeline } from '../types'; + +export function deserializePipelines(pipelinesByName: PipelinesByName): Pipeline[] { + const pipelineNames: string[] = Object.keys(pipelinesByName); + + const deserializedPipelines = pipelineNames.map((name: string) => { + return { + ...pipelinesByName[name], + name, + }; + }); + + return deserializedPipelines; +} diff --git a/x-pack/plugins/ingest_pipelines/common/types.ts b/x-pack/plugins/ingest_pipelines/common/types.ts new file mode 100644 index 0000000000000..8d77359a7c3c5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/types.ts @@ -0,0 +1,28 @@ +/* + * 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. + */ + +interface Processor { + [key: string]: { + [key: string]: unknown; + }; +} + +export interface Pipeline { + name: string; + description: string; + version?: number; + processors: Processor[]; + on_failure?: Processor[]; +} + +export interface PipelinesByName { + [key: string]: { + description: string; + version?: number; + processors: Processor[]; + on_failure?: Processor[]; + }; +} diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json new file mode 100644 index 0000000000000..ec02c5f80edf9 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -0,0 +1,17 @@ +{ + "id": "ingestPipelines", + "version": "8.0.0", + "server": true, + "ui": true, + "requiredPlugins": [ + "licensing", + "management" + ], + "optionalPlugins": [ + "usageCollection" + ], + "configPath": [ + "xpack", + "ingest_pipelines" + ] +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx new file mode 100644 index 0000000000000..ba7675b507596 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -0,0 +1,101 @@ +/* + * 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 { EuiPageContent } from '@elastic/eui'; +import React, { FunctionComponent } from 'react'; +import { HashRouter, Switch, Route } from 'react-router-dom'; + +import { BASE_PATH, APP_CLUSTER_REQUIRED_PRIVILEGES } from '../../common/constants'; + +import { + SectionError, + useAuthorizationContext, + WithPrivileges, + SectionLoading, + NotAuthorizedSection, +} from '../shared_imports'; + +import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections'; + +export const AppWithoutRouter = () => ( + + + + + + {/* Catch all */} + + +); + +export const App: FunctionComponent = () => { + const { apiError } = useAuthorizationContext(); + + if (apiError) { + return ( + + } + error={apiError} + /> + ); + } + + return ( + `cluster.${privilege}`)} + > + {({ isLoading, hasPrivileges, privilegesMissing }) => { + if (isLoading) { + return ( + + + + ); + } + + if (!hasPrivileges) { + return ( + + + } + message={ + + } + /> + + ); + } + + return ( + + + + ); + }} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts new file mode 100644 index 0000000000000..21a2ee30a84e1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelineForm } from './pipeline_form'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts new file mode 100644 index 0000000000000..2b007a25667a1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelineFormProvider as PipelineForm } from './pipeline_form_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx new file mode 100644 index 0000000000000..9082196a48b39 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -0,0 +1,166 @@ +/* + * 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, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; + +import { useForm, Form, FormConfig } from '../../../shared_imports'; +import { Pipeline } from '../../../../common/types'; + +import { PipelineRequestFlyout } from './pipeline_request_flyout'; +import { PipelineTestFlyout } from './pipeline_test_flyout'; +import { PipelineFormFields } from './pipeline_form_fields'; +import { PipelineFormError } from './pipeline_form_error'; +import { pipelineFormSchema } from './schema'; + +export interface PipelineFormProps { + onSave: (pipeline: Pipeline) => void; + onCancel: () => void; + isSaving: boolean; + saveError: any; + defaultValue?: Pipeline; + isEditing?: boolean; +} + +export const PipelineForm: React.FunctionComponent = ({ + defaultValue = { + name: '', + description: '', + processors: '', + on_failure: '', + version: '', + }, + onSave, + isSaving, + saveError, + isEditing, + onCancel, +}) => { + const [isRequestVisible, setIsRequestVisible] = useState(false); + + const [isTestingPipeline, setIsTestingPipeline] = useState(false); + + const handleSave: FormConfig['onSubmit'] = (formData, isValid) => { + if (isValid) { + onSave(formData as Pipeline); + } + }; + + const handleTestPipelineClick = () => { + setIsTestingPipeline(true); + }; + + const { form } = useForm({ + schema: pipelineFormSchema, + defaultValue, + onSubmit: handleSave, + }); + + const saveButtonLabel = isSaving ? ( + + ) : isEditing ? ( + + ) : ( + + ); + + return ( + <> +
+ {/* Request error */} + {saveError && } + + {/* All form fields */} + + + {/* Form submission */} + + + + + + {saveButtonLabel} + + + + + + + + + + + setIsRequestVisible(prevIsRequestVisible => !prevIsRequestVisible)} + > + {isRequestVisible ? ( + + ) : ( + + )} + + + + + {/* ES request flyout */} + {isRequestVisible ? ( + setIsRequestVisible(prevIsRequestVisible => !prevIsRequestVisible)} + /> + ) : null} + + {/* Test pipeline flyout */} + {isTestingPipeline ? ( + { + setIsTestingPipeline(prevIsTestingPipeline => !prevIsTestingPipeline); + }} + /> + ) : null} + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.tsx new file mode 100644 index 0000000000000..ef0e2737df24d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; + +interface Props { + errorMessage: string; +} + +export const PipelineFormError: React.FunctionComponent = ({ errorMessage }) => { + return ( + <> + + } + color="danger" + iconType="alert" + data-test-subj="savePipelineError" + > +

{errorMessage}

+
+ + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx new file mode 100644 index 0000000000000..045afd52204fa --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -0,0 +1,223 @@ +/* + * 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, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiSpacer, EuiSwitch, EuiLink } from '@elastic/eui'; + +import { + getUseField, + getFormRow, + Field, + JsonEditorField, + useKibana, +} from '../../../shared_imports'; + +interface Props { + hasVersion: boolean; + hasOnFailure: boolean; + isTestButtonDisabled: boolean; + onTestPipelineClick: () => void; + isEditing?: boolean; +} + +const UseField = getUseField({ component: Field }); +const FormRow = getFormRow({ titleTag: 'h3' }); + +export const PipelineFormFields: React.FunctionComponent = ({ + isEditing, + hasVersion, + hasOnFailure, + isTestButtonDisabled, + onTestPipelineClick, +}) => { + const { services } = useKibana(); + + const [isVersionVisible, setIsVersionVisible] = useState(hasVersion); + const [isOnFailureEditorVisible, setIsOnFailureEditorVisible] = useState(hasOnFailure); + + return ( + <> + {/* Name field with optional version field */} + } + description={ + <> + + + + } + checked={isVersionVisible} + onChange={e => setIsVersionVisible(e.target.checked)} + data-test-subj="versionToggle" + /> + + } + > + + + {isVersionVisible && ( + + )} + + + {/* Description field */} + + } + description={ + + } + > + + + + {/* Processors field */} + + } + description={ + <> + + {i18n.translate('xpack.ingestPipelines.form.processorsDocumentionLink', { + defaultMessage: 'Learn more.', + })} + + ), + }} + /> + + + + + + + + } + > + + + + {/* On-failure field */} + + } + description={ + <> + + {i18n.translate('xpack.ingestPipelines.form.onFailureDocumentionLink', { + defaultMessage: 'Learn more.', + })} + + ), + }} + /> + + + } + checked={isOnFailureEditorVisible} + onChange={e => setIsOnFailureEditorVisible(e.target.checked)} + data-test-subj="onFailureToggle" + /> + + } + > + {isOnFailureEditorVisible ? ( + + ) : ( + // requires children or a field + // For now, we return an empty
if the editor is not visible +
+ )} + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx new file mode 100644 index 0000000000000..57abea2309aa1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx @@ -0,0 +1,18 @@ +/* + * 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 { PipelineForm as PipelineFormUI, PipelineFormProps } from './pipeline_form'; +import { TestConfigContextProvider } from './test_config_context'; + +export const PipelineFormProvider: React.FunctionComponent = passThroughProps => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts new file mode 100644 index 0000000000000..9476b65557c54 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelineRequestFlyoutProvider as PipelineRequestFlyout } from './pipeline_request_flyout_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx new file mode 100644 index 0000000000000..58e86695808b1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx @@ -0,0 +1,89 @@ +/* + * 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, { useRef } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiButtonEmpty, + EuiCodeBlock, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { Pipeline } from '../../../../../common/types'; + +interface Props { + pipeline: Pipeline; + closeFlyout: () => void; +} + +export const PipelineRequestFlyout: React.FunctionComponent = ({ + closeFlyout, + pipeline, +}) => { + const { name, ...pipelineBody } = pipeline; + const endpoint = `PUT _ingest/pipeline/${name || ''}`; + const payload = JSON.stringify(pipelineBody, null, 2); + const request = `${endpoint}\n${payload}`; + // Hack so that copied-to-clipboard value updates as content changes + // Related issue: https://github.com/elastic/eui/issues/3321 + const uuid = useRef(0); + uuid.current++; + + return ( + + + +

+ {name ? ( + + ) : ( + + )} +

+
+
+ + + +

+ +

+
+ + + + {request} + +
+ + + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx new file mode 100644 index 0000000000000..6dcedca6085af --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect } from 'react'; + +import { Pipeline } from '../../../../../common/types'; +import { useFormContext } from '../../../../shared_imports'; +import { PipelineRequestFlyout } from './pipeline_request_flyout'; + +export const PipelineRequestFlyoutProvider = ({ closeFlyout }: { closeFlyout: () => void }) => { + const form = useFormContext(); + const [formData, setFormData] = useState({} as Pipeline); + + useEffect(() => { + const subscription = form.subscribe(async ({ isValid, validate, data }) => { + const isFormValid = isValid ?? (await validate()); + if (isFormValid) { + setFormData(data.format() as Pipeline); + } + }); + + return subscription.unsubscribe; + }, [form]); + + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/index.ts new file mode 100644 index 0000000000000..38bbc43b469a5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelineTestFlyoutProvider as PipelineTestFlyout } from './pipeline_test_flyout_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx new file mode 100644 index 0000000000000..16f39b2912c1d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx @@ -0,0 +1,203 @@ +/* + * 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, { useState, useEffect, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, + EuiCallOut, +} from '@elastic/eui'; + +import { useKibana } from '../../../../shared_imports'; +import { Pipeline } from '../../../../../common/types'; +import { Tabs, Tab, OutputTab, DocumentsTab } from './tabs'; +import { useTestConfigContext } from '../test_config_context'; + +export interface PipelineTestFlyoutProps { + closeFlyout: () => void; + pipeline: Pipeline; + isPipelineValid: boolean; +} + +export const PipelineTestFlyout: React.FunctionComponent = ({ + closeFlyout, + pipeline, + isPipelineValid, +}) => { + const { services } = useKibana(); + + const { testConfig } = useTestConfigContext(); + const { documents: cachedDocuments, verbose: cachedVerbose } = testConfig; + + const initialSelectedTab = cachedDocuments ? 'output' : 'documents'; + const [selectedTab, setSelectedTab] = useState(initialSelectedTab); + + const [shouldExecuteImmediately, setShouldExecuteImmediately] = useState(false); + const [isExecuting, setIsExecuting] = useState(false); + const [executeError, setExecuteError] = useState(null); + const [executeOutput, setExecuteOutput] = useState(undefined); + + const handleExecute = useCallback( + async (documents: object[], verbose?: boolean) => { + const { name: pipelineName, ...pipelineDefinition } = pipeline; + + setIsExecuting(true); + setExecuteError(null); + + const { error, data: output } = await services.api.simulatePipeline({ + documents, + verbose, + pipeline: pipelineDefinition, + }); + + setIsExecuting(false); + + if (error) { + setExecuteError(error); + return; + } + + setExecuteOutput(output); + + services.notifications.toasts.addSuccess( + i18n.translate('xpack.ingestPipelines.testPipelineFlyout.successNotificationText', { + defaultMessage: 'Pipeline executed', + }), + { + toastLifeTimeMs: 1000, + } + ); + + setSelectedTab('output'); + }, + [pipeline, services.api, services.notifications.toasts] + ); + + useEffect(() => { + if (cachedDocuments) { + setShouldExecuteImmediately(true); + } + // We only want to know on initial mount if there are cached documents + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + // If the user has already tested the pipeline once, + // use the cached test config and automatically execute the pipeline + if (shouldExecuteImmediately && Object.entries(pipeline).length > 0) { + setShouldExecuteImmediately(false); + handleExecute(cachedDocuments!, cachedVerbose); + } + }, [ + pipeline, + handleExecute, + cachedDocuments, + cachedVerbose, + isExecuting, + shouldExecuteImmediately, + ]); + + let tabContent; + + if (selectedTab === 'output') { + tabContent = ( + + ); + } else { + // default to "documents" tab + tabContent = ( + + ); + } + + return ( + + + +

+ {pipeline.name ? ( + + ) : ( + + )} +

+
+
+ + + !executeOutput && tabId === 'output'} + /> + + + + {/* Execute error */} + {executeError ? ( + <> + + } + color="danger" + iconType="alert" + > +

{executeError.message}

+
+ + + ) : null} + + {/* Invalid pipeline error */} + {!isPipelineValid ? ( + <> + + } + color="danger" + iconType="alert" + /> + + + ) : null} + + {/* Documents or output tab content */} + {tabContent} +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx new file mode 100644 index 0000000000000..351478394595a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.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. + */ + +import React, { useState, useEffect } from 'react'; + +import { Pipeline } from '../../../../../common/types'; +import { useFormContext } from '../../../../shared_imports'; +import { PipelineTestFlyout, PipelineTestFlyoutProps } from './pipeline_test_flyout'; + +type Props = Omit; + +export const PipelineTestFlyoutProvider: React.FunctionComponent = ({ closeFlyout }) => { + const form = useFormContext(); + const [formData, setFormData] = useState({} as Pipeline); + const [isFormDataValid, setIsFormDataValid] = useState(false); + + useEffect(() => { + const subscription = form.subscribe(async ({ isValid, validate, data }) => { + const isFormValid = isValid ?? (await validate()); + if (isFormValid) { + setFormData(data.format() as Pipeline); + } + setIsFormDataValid(isFormValid); + }); + + return subscription.unsubscribe; + }, [form]); + + return ( + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts new file mode 100644 index 0000000000000..ea8fe2cd92350 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Tabs, Tab } from './pipeline_test_tabs'; + +export { DocumentsTab } from './tab_documents'; + +export { OutputTab } from './tab_output'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx new file mode 100644 index 0000000000000..f720b80122702 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx @@ -0,0 +1,60 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTab, EuiTabs } from '@elastic/eui'; + +export type Tab = 'documents' | 'output'; + +interface Props { + onTabChange: (tab: Tab) => void; + selectedTab: Tab; + getIsDisabled: (tab: Tab) => boolean; +} + +export const Tabs: React.FunctionComponent = ({ + onTabChange, + selectedTab, + getIsDisabled, +}) => { + const tabs: Array<{ + id: Tab; + name: React.ReactNode; + }> = [ + { + id: 'documents', + name: ( + + ), + }, + { + id: 'output', + name: ( + + ), + }, + ]; + + return ( + + {tabs.map(tab => ( + onTabChange(tab.id)} + isSelected={tab.id === selectedTab} + key={tab.id} + disabled={getIsDisabled(tab.id)} + data-test-subj={tab.id.toLowerCase() + '_tab'} + > + {tab.name} + + ))} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx new file mode 100644 index 0000000000000..de9910344bd4b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx @@ -0,0 +1,87 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiCode } from '@elastic/eui'; + +import { FormSchema, fieldValidators, ValidationFuncArg } from '../../../../../shared_imports'; +import { parseJson, stringifyJson } from '../../../../lib'; + +const { emptyField, isJsonField } = fieldValidators; + +export const documentsSchema: FormSchema = { + documents: { + label: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel', + { + defaultMessage: 'Documents', + } + ), + helpText: ( + + {JSON.stringify([ + { + _index: 'index', + _id: 'id', + _source: { + foo: 'bar', + }, + }, + ])} + + ), + }} + /> + ), + serializer: parseJson, + deserializer: stringifyJson, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.noDocumentsError', + { + defaultMessage: 'Documents are required.', + } + ) + ), + }, + { + validator: isJsonField( + i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError', + { + defaultMessage: 'The documents JSON is not valid.', + } + ) + ), + }, + { + validator: ({ value }: ValidationFuncArg) => { + const parsedJSON = JSON.parse(value); + + if (!parsedJSON.length) { + return { + message: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.oneDocumentRequiredError', + { + defaultMessage: 'At least one document is required.', + } + ), + }; + } + }, + }, + ], + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx new file mode 100644 index 0000000000000..97bf03dbdc068 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx @@ -0,0 +1,152 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { EuiSpacer, EuiText, EuiButton, EuiHorizontalRule, EuiLink } from '@elastic/eui'; + +import { + getUseField, + Field, + JsonEditorField, + Form, + useForm, + FormConfig, + useKibana, +} from '../../../../../shared_imports'; + +import { documentsSchema } from './schema'; +import { useTestConfigContext, TestConfig } from '../../test_config_context'; + +const UseField = getUseField({ component: Field }); + +interface Props { + handleExecute: (documents: object[], verbose: boolean) => void; + isPipelineValid: boolean; + isExecuting: boolean; +} + +export const DocumentsTab: React.FunctionComponent = ({ + isPipelineValid, + handleExecute, + isExecuting, +}) => { + const { services } = useKibana(); + + const { setCurrentTestConfig, testConfig } = useTestConfigContext(); + const { verbose: cachedVerbose, documents: cachedDocuments } = testConfig; + + const executePipeline: FormConfig['onSubmit'] = (formData, isValid) => { + if (!isValid || !isPipelineValid) { + return; + } + + const { documents } = formData as TestConfig; + + // Update context + setCurrentTestConfig({ + ...testConfig, + documents, + }); + + handleExecute(documents!, cachedVerbose); + }; + + const { form } = useForm({ + schema: documentsSchema, + defaultValue: { + documents: cachedDocuments || '', + verbose: cachedVerbose || false, + }, + onSubmit: executePipeline, + }); + + return ( + <> + +

+ + {i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> +

+
+ + + +
+ {/* Documents editor */} + + + + + +

+ +

+
+ + + + + {isExecuting ? ( + + ) : ( + + )} + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_output.tsx new file mode 100644 index 0000000000000..aa80f8c86ad8b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_output.tsx @@ -0,0 +1,106 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiCodeBlock, + EuiSpacer, + EuiText, + EuiSwitch, + EuiLink, + EuiIcon, + EuiLoadingSpinner, + EuiIconTip, +} from '@elastic/eui'; +import { useTestConfigContext } from '../../test_config_context'; + +interface Props { + executeOutput?: { docs: object[] }; + handleExecute: (documents: object[], verbose: boolean) => void; + isExecuting: boolean; +} + +export const OutputTab: React.FunctionComponent = ({ + executeOutput, + handleExecute, + isExecuting, +}) => { + const { setCurrentTestConfig, testConfig } = useTestConfigContext(); + const { verbose: cachedVerbose, documents: cachedDocuments } = testConfig; + + const onEnableVerbose = (isVerboseEnabled: boolean) => { + setCurrentTestConfig({ + ...testConfig, + verbose: isVerboseEnabled, + }); + + handleExecute(cachedDocuments!, isVerboseEnabled); + }; + + let content: React.ReactNode | undefined; + + if (isExecuting) { + content = ; + } else if (executeOutput) { + content = ( + + {JSON.stringify(executeOutput, null, 2)} + + ); + } + + return ( + <> + +

+ handleExecute(cachedDocuments!, cachedVerbose)}> + {' '} + + + ), + }} + /> +

+
+ + + + + {' '} + + } + /> + + } + checked={cachedVerbose} + onChange={e => onEnableVerbose(e.target.checked)} + /> + + + + {content} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx new file mode 100644 index 0000000000000..2e2689f41527a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx @@ -0,0 +1,147 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; + +import { FormSchema, FIELD_TYPES, fieldValidators, fieldFormatters } from '../../../shared_imports'; +import { parseJson, stringifyJson } from '../../lib'; + +const { emptyField, isJsonField } = fieldValidators; +const { toInt } = fieldFormatters; + +export const pipelineFormSchema: FormSchema = { + name: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.form.nameFieldLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.form.pipelineNameRequiredError', { + defaultMessage: 'A pipeline name is required.', + }) + ), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate('xpack.ingestPipelines.form.descriptionFieldLabel', { + defaultMessage: 'Description', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.form.pipelineDescriptionRequiredError', { + defaultMessage: 'A pipeline description is required.', + }) + ), + }, + ], + }, + processors: { + label: i18n.translate('xpack.ingestPipelines.form.processorsFieldLabel', { + defaultMessage: 'Processors', + }), + helpText: ( + + {JSON.stringify([ + { + set: { + field: 'foo', + value: 'bar', + }, + }, + ])} + + ), + }} + /> + ), + serializer: parseJson, + deserializer: stringifyJson, + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.form.processorsRequiredError', { + defaultMessage: 'Processors are required.', + }) + ), + }, + { + validator: isJsonField( + i18n.translate('xpack.ingestPipelines.form.processorsJsonError', { + defaultMessage: 'The processors JSON is not valid.', + }) + ), + }, + ], + }, + on_failure: { + label: i18n.translate('xpack.ingestPipelines.form.onFailureFieldLabel', { + defaultMessage: 'On-failure processors (optional)', + }), + helpText: ( + + {JSON.stringify([ + { + set: { + field: '_index', + value: 'failed-{{ _index }}', + }, + }, + ])} + + ), + }} + /> + ), + serializer: value => { + const result = parseJson(value); + // If an empty array was passed, strip out this value entirely. + if (!result.length) { + return undefined; + } + return result; + }, + deserializer: stringifyJson, + validations: [ + { + validator: validationArg => { + if (!validationArg.value) { + return; + } + return isJsonField( + i18n.translate('xpack.ingestPipelines.form.onFailureProcessorsJsonError', { + defaultMessage: 'The on-failure processors JSON is not valid.', + }) + )(validationArg); + }, + }, + ], + }, + version: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate('xpack.ingestPipelines.form.versionFieldLabel', { + defaultMessage: 'Version (optional)', + }), + formatters: [toInt], + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/test_config_context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/test_config_context.tsx new file mode 100644 index 0000000000000..6840ebef28796 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/test_config_context.tsx @@ -0,0 +1,57 @@ +/* + * 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, { useState, useCallback, useContext } from 'react'; + +export interface TestConfig { + documents?: object[] | undefined; + verbose: boolean; +} + +interface TestConfigContext { + testConfig: TestConfig; + setCurrentTestConfig: (config: TestConfig) => void; +} + +const TEST_CONFIG_DEFAULT_VALUE = { + testConfig: { + verbose: false, + }, + setCurrentTestConfig: () => {}, +}; + +const TestConfigContext = React.createContext(TEST_CONFIG_DEFAULT_VALUE); + +export const useTestConfigContext = () => { + const ctx = useContext(TestConfigContext); + if (!ctx) { + throw new Error( + '"useTestConfigContext" can only be called inside of TestConfigContext.Provider!' + ); + } + return ctx; +}; + +export const TestConfigContextProvider = ({ children }: { children: React.ReactNode }) => { + const [testConfig, setTestConfig] = useState({ + verbose: false, + }); + + const setCurrentTestConfig = useCallback((currentTestConfig: TestConfig): void => { + setTestConfig(currentTestConfig); + }, []); + + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts new file mode 100644 index 0000000000000..776d44c825670 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// UI metric constants +export const UIM_APP_NAME = 'ingest_pipelines'; +export const UIM_PIPELINES_LIST_LOAD = 'pipelines_list_load'; +export const UIM_PIPELINE_CREATE = 'pipeline_create'; +export const UIM_PIPELINE_UPDATE = 'pipeline_update'; +export const UIM_PIPELINE_DELETE = 'pipeline_delete'; +export const UIM_PIPELINE_DELETE_MANY = 'pipeline_delete_many'; +export const UIM_PIPELINE_SIMULATE = 'pipeline_simulate'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/index.tsx b/x-pack/plugins/ingest_pipelines/public/application/index.tsx new file mode 100644 index 0000000000000..e43dba4689b44 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/index.tsx @@ -0,0 +1,55 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; +import React, { ReactNode } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { NotificationsSetup } from 'kibana/public'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; + +import { API_BASE_PATH } from '../../common/constants'; + +import { AuthorizationProvider } from '../shared_imports'; + +import { App } from './app'; +import { DocumentationService, UiMetricService, ApiService, BreadcrumbService } from './services'; + +export interface AppServices { + breadcrumbs: BreadcrumbService; + metric: UiMetricService; + documentation: DocumentationService; + api: ApiService; + notifications: NotificationsSetup; +} + +export interface CoreServices { + http: HttpSetup; +} + +export const renderApp = ( + element: HTMLElement, + I18nContext: ({ children }: { children: ReactNode }) => JSX.Element, + services: AppServices, + coreServices: CoreServices +) => { + render( + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/lib/index.ts b/x-pack/plugins/ingest_pipelines/public/application/lib/index.ts new file mode 100644 index 0000000000000..1283033267a50 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { stringifyJson, parseJson } from './utils'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/lib/utils.test.ts b/x-pack/plugins/ingest_pipelines/public/application/lib/utils.test.ts new file mode 100644 index 0000000000000..e7eff3bd6ca33 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/lib/utils.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { stringifyJson, parseJson } from './utils'; + +describe('utils', () => { + describe('stringifyJson()', () => { + it('should stringify a valid JSON array', () => { + expect(stringifyJson([1, 2, 3])).toEqual(`[ + 1, + 2, + 3 +]`); + }); + + it('should return a stringified empty array if the value is not a valid JSON array', () => { + expect(stringifyJson({})).toEqual('[\n\n]'); + }); + }); + + describe('parseJson()', () => { + it('should parse a valid JSON string', () => { + expect(parseJson('[1,2,3]')).toEqual([1, 2, 3]); + expect(parseJson('[{"foo": "bar"}]')).toEqual([{ foo: 'bar' }]); + }); + + it('should convert valid JSON that is not an array to an array', () => { + expect(parseJson('{"foo": "bar"}')).toEqual([{ foo: 'bar' }]); + }); + + it('should return an empty array if invalid JSON string', () => { + expect(parseJson('{invalidJsonString}')).toEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/lib/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/lib/utils.ts new file mode 100644 index 0000000000000..fe4e9e65f4b9a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/lib/utils.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. + */ + +export const stringifyJson = (json: any): string => + Array.isArray(json) ? JSON.stringify(json, null, 2) : '[\n\n]'; + +export const parseJson = (jsonString: string): object[] => { + let parsedJSON: any; + + try { + parsedJSON = JSON.parse(jsonString); + + if (!Array.isArray(parsedJSON)) { + // Convert object to array + parsedJSON = [parsedJSON]; + } + } catch { + parsedJSON = []; + } + + return parsedJSON; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts new file mode 100644 index 0000000000000..e36f27cbf5f62 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { CoreSetup } from 'src/core/public'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; + +import { documentationService, uiMetricService, apiService, breadcrumbService } from './services'; +import { renderApp } from '.'; + +export async function mountManagementSection( + { http, getStartServices, notifications }: CoreSetup, + params: ManagementAppMountParams +) { + const { element, setBreadcrumbs } = params; + const [coreStart] = await getStartServices(); + const { + docLinks, + i18n: { Context: I18nContext }, + } = coreStart; + + documentationService.setup(docLinks); + breadcrumbService.setup(setBreadcrumbs); + + const services = { + breadcrumbs: breadcrumbService, + metric: uiMetricService, + documentation: documentationService, + api: apiService, + notifications, + }; + + return renderApp(element, I18nContext, services, { http }); +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts new file mode 100644 index 0000000000000..b2925666c5768 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/index.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. + */ + +export { PipelinesList } from './pipelines_list'; + +export { PipelinesCreate } from './pipelines_create'; + +export { PipelinesEdit } from './pipelines_edit'; + +export { PipelinesClone } from './pipelines_clone'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts new file mode 100644 index 0000000000000..614a3598d407d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelinesClone } from './pipelines_clone'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx new file mode 100644 index 0000000000000..b3b1217caf834 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx @@ -0,0 +1,59 @@ +/* + * 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, { FunctionComponent, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { SectionLoading, useKibana } from '../../../shared_imports'; + +import { PipelinesCreate } from '../pipelines_create'; + +export interface ParamProps { + sourceName: string; +} + +/** + * This section is a wrapper around the create section where we receive a pipeline name + * to load and set as the source pipeline for the {@link PipelinesCreate} form. + */ +export const PipelinesClone: FunctionComponent> = props => { + const { sourceName } = props.match.params; + const { services } = useKibana(); + + const { error, data: pipeline, isLoading, isInitialRequest } = services.api.useLoadPipeline( + decodeURIComponent(sourceName) + ); + + useEffect(() => { + if (error && !isLoading) { + services.notifications!.toasts.addError(error, { + title: i18n.translate('xpack.ingestPipelines.clone.loadSourcePipelineErrorTitle', { + defaultMessage: 'Cannot load {name}.', + values: { name: sourceName }, + }), + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading]); + + if (isLoading && isInitialRequest) { + return ( + + + + ); + } else { + // We still show the create form even if we were not able to load the + // latest pipeline data. + const sourcePipeline = pipeline ? { ...pipeline, name: `${pipeline.name}-copy` } : undefined; + return ; + } +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts new file mode 100644 index 0000000000000..374defa869916 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelinesCreate } from './pipelines_create'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx new file mode 100644 index 0000000000000..34a362d596d92 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -0,0 +1,109 @@ +/* + * 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, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiPageBody, + EuiPageContent, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, +} from '@elastic/eui'; + +import { BASE_PATH } from '../../../../common/constants'; +import { Pipeline } from '../../../../common/types'; +import { useKibana } from '../../../shared_imports'; +import { PipelineForm } from '../../components'; + +interface Props { + /** + * This value may be passed in to prepopulate the creation form + */ + sourcePipeline?: Pipeline; +} + +export const PipelinesCreate: React.FunctionComponent = ({ + history, + sourcePipeline, +}) => { + const { services } = useKibana(); + + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + const onSave = async (pipeline: Pipeline) => { + setIsSaving(true); + setSaveError(null); + + const { error } = await services.api.createPipeline(pipeline); + + setIsSaving(false); + + if (error) { + setSaveError(error); + return; + } + + history.push(BASE_PATH + `?pipeline=${pipeline.name}`); + }; + + const onCancel = () => { + history.push(BASE_PATH); + }; + + useEffect(() => { + services.breadcrumbs.setBreadcrumbs('create'); + }, [services]); + + return ( + + + + + + +

+ +

+
+
+ + + + + + +
+
+ + + + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts new file mode 100644 index 0000000000000..26458d23fd6d8 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelinesEdit } from './pipelines_edit'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx new file mode 100644 index 0000000000000..99cd8d7eef97b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx @@ -0,0 +1,151 @@ +/* + * 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, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; + +import { EuiCallOut } from '@elastic/eui'; +import { BASE_PATH } from '../../../../common/constants'; +import { Pipeline } from '../../../../common/types'; +import { useKibana, SectionLoading } from '../../../shared_imports'; +import { PipelineForm } from '../../components'; + +interface MatchParams { + name: string; +} + +export const PipelinesEdit: React.FunctionComponent> = ({ + match: { + params: { name }, + }, + history, +}) => { + const { services } = useKibana(); + + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + const decodedPipelineName = decodeURI(decodeURIComponent(name)); + + const { error, data: pipeline, isLoading } = services.api.useLoadPipeline(decodedPipelineName); + + const onSave = async (updatedPipeline: Pipeline) => { + setIsSaving(true); + setSaveError(null); + + const { error: savePipelineError } = await services.api.updatePipeline(updatedPipeline); + + setIsSaving(false); + + if (savePipelineError) { + setSaveError(savePipelineError); + return; + } + + history.push(BASE_PATH + `?pipeline=${updatedPipeline.name}`); + }; + + const onCancel = () => { + history.push(BASE_PATH); + }; + + useEffect(() => { + services.breadcrumbs.setBreadcrumbs('edit'); + }, [services.breadcrumbs]); + + let content: React.ReactNode; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + <> + + } + color="danger" + iconType="alert" + data-test-subj="fetchPipelineError" + > +

{error.message}

+
+ + + ); + } else if (pipeline) { + content = ( + + ); + } + + return ( + + + + + + +

+ +

+
+
+ + + + + + +
+
+ + + + {content} +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx new file mode 100644 index 0000000000000..c7736a6c19ba1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx @@ -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 React from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { useKibana } from '../../../shared_imports'; + +export const PipelineDeleteModal = ({ + pipelinesToDelete, + callback, +}: { + pipelinesToDelete: string[]; + callback: (data?: { hasDeletedPipelines: boolean }) => void; +}) => { + const { services } = useKibana(); + + const numPipelinesToDelete = pipelinesToDelete.length; + + const handleDeletePipelines = () => { + services.api + .deletePipelines(pipelinesToDelete) + .then(({ data: { itemsDeleted, errors }, error }) => { + const hasDeletedPipelines = itemsDeleted && itemsDeleted.length; + + if (hasDeletedPipelines) { + const successMessage = + itemsDeleted.length === 1 + ? i18n.translate( + 'xpack.ingestPipelines.deleteModal.successDeleteSingleNotificationMessageText', + { + defaultMessage: "Deleted pipeline '{pipelineName}'", + values: { pipelineName: pipelinesToDelete[0] }, + } + ) + : i18n.translate( + 'xpack.ingestPipelines.deleteModal.successDeleteMultipleNotificationMessageText', + { + defaultMessage: + 'Deleted {numSuccesses, plural, one {# pipeline} other {# pipelines}}', + values: { numSuccesses: itemsDeleted.length }, + } + ); + + callback({ hasDeletedPipelines }); + services.notifications.toasts.addSuccess(successMessage); + } + + if (error || errors?.length) { + const hasMultipleErrors = errors?.length > 1 || (error && pipelinesToDelete.length > 1); + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.ingestPipelines.deleteModal.multipleErrorsNotificationMessageText', + { + defaultMessage: 'Error deleting {count} pipelines', + values: { + count: errors?.length || pipelinesToDelete.length, + }, + } + ) + : i18n.translate('xpack.ingestPipelines.deleteModal.errorNotificationMessageText', { + defaultMessage: "Error deleting pipeline '{name}'", + values: { name: (errors && errors[0].name) || pipelinesToDelete[0] }, + }); + services.notifications.toasts.addDanger(errorMessage); + } + }); + }; + + const handleOnCancel = () => { + callback(); + }; + + return ( + + + } + onCancel={handleOnCancel} + onConfirm={handleDeletePipelines} + cancelButtonText={ + + } + confirmButtonText={ + + } + > + <> +

+ +

+ +
    + {pipelinesToDelete.map(name => ( +
  • {name}
  • + ))} +
+ +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx new file mode 100644 index 0000000000000..98243a5149c0d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx @@ -0,0 +1,210 @@ +/* + * 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, { FunctionComponent, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiIcon, + EuiPopover, + EuiContextMenu, + EuiButton, +} from '@elastic/eui'; + +import { Pipeline } from '../../../../common/types'; + +import { PipelineDetailsJsonBlock } from './details_json_block'; + +export interface Props { + pipeline: Pipeline; + onEditClick: (pipelineName: string) => void; + onCloneClick: (pipelineName: string) => void; + onDeleteClick: (pipelineName: string[]) => void; + onClose: () => void; +} + +export const PipelineDetailsFlyout: FunctionComponent = ({ + pipeline, + onClose, + onEditClick, + onCloneClick, + onDeleteClick, +}) => { + const [showPopover, setShowPopover] = useState(false); + const actionMenuItems = [ + /** + * Edit pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.editActionLabel', { + defaultMessage: 'Edit', + }), + icon: , + onClick: () => onEditClick(pipeline.name), + }, + /** + * Clone pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel', { + defaultMessage: 'Clone', + }), + icon: , + onClick: () => onCloneClick(pipeline.name), + }, + /** + * Delete pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteActionLabel', { + defaultMessage: 'Delete', + }), + icon: , + onClick: () => onDeleteClick([pipeline.name]), + }, + ]; + + const managePipelineButton = ( + setShowPopover(previousBool => !previousBool)} + iconType="arrowUp" + iconSide="right" + fill + > + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.managePipelineButtonLabel', { + defaultMessage: 'Manage', + })} + + ); + + return ( + + + +

{pipeline.name}

+
+
+ + + + {/* Pipeline description */} + + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.descriptionTitle', { + defaultMessage: 'Description', + })} + + + {pipeline.description ?? ''} + + + {/* Pipeline version */} + {pipeline.version && ( + <> + + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.versionTitle', { + defaultMessage: 'Version', + })} + + + {String(pipeline.version)} + + + )} + + {/* Processors JSON */} + + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.processorsTitle', { + defaultMessage: 'Processors JSON', + })} + + + + + + {/* On Failure Processor JSON */} + {pipeline.on_failure?.length && ( + <> + + {i18n.translate( + 'xpack.ingestPipelines.list.pipelineDetails.failureProcessorsTitle', + { + defaultMessage: 'On failure processors JSON', + } + )} + + + + + + )} + + + + + + + + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.closeButtonLabel', { + defaultMessage: 'Close', + })} + + + + + setShowPopover(false)} + button={managePipelineButton} + panelPaddingSize="none" + withTitle + repositionOnScroll + > + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx new file mode 100644 index 0000000000000..6c44336c7547d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx @@ -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. + */ + +import React, { FunctionComponent, useRef } from 'react'; +import { EuiCodeBlock } from '@elastic/eui'; + +export interface Props { + json: Record; +} + +export const PipelineDetailsJsonBlock: FunctionComponent = ({ json }) => { + // Hack so copied-to-clipboard value updates as content changes + // Related issue: https://github.com/elastic/eui/issues/3321 + const uuid = useRef(0); + uuid.current++; + + return ( + 0 ? 300 : undefined} + isCopyable + key={uuid.current} + > + {JSON.stringify(json, null, 2)} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx new file mode 100644 index 0000000000000..ef64fb33a6a55 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { BASE_PATH } from '../../../../common/constants'; + +export const EmptyList: FunctionComponent = () => ( + + {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptTitle', { + defaultMessage: 'Start by creating a pipeline', + })} + + } + actions={ + + {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', { + defaultMessage: 'Create a pipeline', + })} + + } + /> +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts new file mode 100644 index 0000000000000..a541e3bb85fd0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelinesList } from './main'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx new file mode 100644 index 0000000000000..c90ac2714a95a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -0,0 +1,207 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Location } from 'history'; +import { parse } from 'query-string'; + +import { + EuiPageBody, + EuiPageContent, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiCallOut, +} from '@elastic/eui'; + +import { EuiSpacer, EuiText } from '@elastic/eui'; + +import { Pipeline } from '../../../../common/types'; +import { BASE_PATH } from '../../../../common/constants'; +import { useKibana, SectionLoading } from '../../../shared_imports'; +import { UIM_PIPELINES_LIST_LOAD } from '../../constants'; + +import { EmptyList } from './empty_list'; +import { PipelineTable } from './table'; +import { PipelineDetailsFlyout } from './details_flyout'; +import { PipelineNotFoundFlyout } from './not_found_flyout'; +import { PipelineDeleteModal } from './delete_modal'; + +const getPipelineNameFromLocation = (location: Location) => { + const { pipeline } = parse(location.search.substring(1)); + return pipeline; +}; + +export const PipelinesList: React.FunctionComponent = ({ + history, + location, +}) => { + const { services } = useKibana(); + const pipelineNameFromLocation = getPipelineNameFromLocation(location); + + const [selectedPipeline, setSelectedPipeline] = useState(undefined); + const [showFlyout, setShowFlyout] = useState(false); + + const [pipelinesToDelete, setPipelinesToDelete] = useState([]); + + const { data, isLoading, error, sendRequest } = services.api.useLoadPipelines(); + + // Track component loaded + useEffect(() => { + services.metric.trackUiMetric(UIM_PIPELINES_LIST_LOAD); + services.breadcrumbs.setBreadcrumbs('home'); + }, [services.metric, services.breadcrumbs]); + + useEffect(() => { + if (pipelineNameFromLocation && data?.length) { + const pipeline = data.find(p => p.name === pipelineNameFromLocation); + setSelectedPipeline(pipeline); + setShowFlyout(true); + } + }, [pipelineNameFromLocation, data]); + + const goToEditPipeline = (name: string) => { + history.push(`${BASE_PATH}/edit/${encodeURIComponent(name)}`); + }; + + const goToClonePipeline = (name: string) => { + history.push(`${BASE_PATH}/create/${encodeURIComponent(name)}`); + }; + + const goHome = () => { + setShowFlyout(false); + history.push(BASE_PATH); + }; + + let content: React.ReactNode; + + if (isLoading) { + content = ( + + + + ); + } else if (data?.length) { + content = ( + + ); + } else { + content = ; + } + + const renderFlyout = (): React.ReactNode => { + if (!showFlyout) { + return; + } + if (selectedPipeline) { + return ( + { + setSelectedPipeline(undefined); + goHome(); + }} + onEditClick={goToEditPipeline} + onCloneClick={goToClonePipeline} + onDeleteClick={setPipelinesToDelete} + /> + ); + } else { + // Somehow we triggered show pipeline details, but do not have a pipeline. + // We assume not found. + return ( + { + goHome(); + }} + pipelineName={pipelineNameFromLocation} + /> + ); + } + }; + + return ( + <> + + + + + +

+ +

+
+ + + + + +
+
+ + + + + + + + {/* Error call out for pipeline table */} + {error ? ( + + ) : ( + content + )} +
+
+ {renderFlyout()} + {pipelinesToDelete?.length > 0 ? ( + { + if (deleteResponse?.hasDeletedPipelines) { + // reload pipelines list + sendRequest(); + } + setPipelinesToDelete([]); + setSelectedPipeline(undefined); + }} + pipelinesToDelete={pipelinesToDelete} + /> + ) : null} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx new file mode 100644 index 0000000000000..b967e54187ced --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx @@ -0,0 +1,42 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlyout, EuiFlyoutBody, EuiCallOut } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; + +interface Props { + onClose: () => void; + pipelineName: string | string[] | null | undefined; +} + +export const PipelineNotFoundFlyout: FunctionComponent = ({ onClose, pipelineName }) => { + return ( + + + {pipelineName && ( + +

{pipelineName}

+
+ )} +
+ + + + } + color="danger" + iconType="alert" + /> + +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx new file mode 100644 index 0000000000000..c93285289ff39 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -0,0 +1,148 @@ +/* + * 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, { FunctionComponent, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiInMemoryTable, EuiLink, EuiButton, EuiInMemoryTableProps } from '@elastic/eui'; + +import { BASE_PATH } from '../../../../common/constants'; +import { Pipeline } from '../../../../common/types'; + +export interface Props { + pipelines: Pipeline[]; + onReloadClick: () => void; + onEditPipelineClick: (pipelineName: string) => void; + onClonePipelineClick: (pipelineName: string) => void; + onDeletePipelineClick: (pipelineName: string[]) => void; +} + +export const PipelineTable: FunctionComponent = ({ + pipelines, + onReloadClick, + onEditPipelineClick, + onClonePipelineClick, + onDeletePipelineClick, +}) => { + const [selection, setSelection] = useState([]); + + const tableProps: EuiInMemoryTableProps = { + itemId: 'name', + isSelectable: true, + sorting: { sort: { field: 'name', direction: 'asc' } }, + selection: { + onSelectionChange: setSelection, + }, + search: { + toolsLeft: + selection.length > 0 ? ( + onDeletePipelineClick(selection.map(pipeline => pipeline.name))} + color="danger" + > + + + ) : ( + undefined + ), + toolsRight: [ + + {i18n.translate('xpack.ingestPipelines.list.table.reloadButtonLabel', { + defaultMessage: 'Reload', + })} + , + + {i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', { + defaultMessage: 'Create a pipeline', + })} + , + ], + box: { + incremental: true, + }, + }, + pagination: { + initialPageSize: 10, + pageSizeOptions: [10, 20, 50], + }, + columns: [ + { + field: 'name', + name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', { + defaultMessage: 'Name', + }), + sortable: true, + render: (name: string) => {name}, + }, + { + name: ( + + ), + actions: [ + { + isPrimary: true, + name: i18n.translate('xpack.ingestPipelines.list.table.editActionLabel', { + defaultMessage: 'Edit', + }), + description: i18n.translate('xpack.ingestPipelines.list.table.editActionDescription', { + defaultMessage: 'Edit this pipeline', + }), + type: 'icon', + icon: 'pencil', + onClick: ({ name }) => onEditPipelineClick(name), + }, + { + name: i18n.translate('xpack.ingestPipelines.list.table.cloneActionLabel', { + defaultMessage: 'Clone', + }), + description: i18n.translate('xpack.ingestPipelines.list.table.cloneActionDescription', { + defaultMessage: 'Clone this pipeline', + }), + type: 'icon', + icon: 'copy', + onClick: ({ name }) => onClonePipelineClick(name), + }, + { + isPrimary: true, + name: i18n.translate('xpack.ingestPipelines.list.table.deleteActionLabel', { + defaultMessage: 'Delete', + }), + description: i18n.translate( + 'xpack.ingestPipelines.list.table.deleteActionDescription', + { defaultMessage: 'Delete this pipeline' } + ), + type: 'icon', + icon: 'trash', + color: 'danger', + onClick: ({ name }) => onDeletePipelineClick([name]), + }, + ], + }, + ], + items: pipelines ?? [], + }; + + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts new file mode 100644 index 0000000000000..13eb96e78adae --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/api.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 { HttpSetup } from 'src/core/public'; + +import { Pipeline } from '../../../common/types'; +import { API_BASE_PATH } from '../../../common/constants'; +import { + UseRequestConfig, + SendRequestConfig, + SendRequestResponse, + sendRequest as _sendRequest, + useRequest as _useRequest, +} from '../../shared_imports'; +import { UiMetricService } from './ui_metric'; +import { + UIM_PIPELINE_CREATE, + UIM_PIPELINE_UPDATE, + UIM_PIPELINE_DELETE, + UIM_PIPELINE_DELETE_MANY, + UIM_PIPELINE_SIMULATE, +} from '../constants'; + +export class ApiService { + private client: HttpSetup | undefined; + private uiMetricService: UiMetricService | undefined; + + private useRequest(config: UseRequestConfig) { + if (!this.client) { + throw new Error('Api service has not be initialized.'); + } + return _useRequest(this.client, config); + } + + private sendRequest( + config: SendRequestConfig + ): Promise> { + if (!this.client) { + throw new Error('Api service has not be initialized.'); + } + return _sendRequest(this.client, config); + } + + private trackUiMetric(eventName: string) { + if (!this.uiMetricService) { + throw new Error('UI metric service has not be initialized.'); + } + return this.uiMetricService.trackUiMetric(eventName); + } + + public setup(httpClient: HttpSetup, uiMetricService: UiMetricService): void { + this.client = httpClient; + this.uiMetricService = uiMetricService; + } + + public useLoadPipelines() { + return this.useRequest({ + path: API_BASE_PATH, + method: 'get', + }); + } + + public useLoadPipeline(name: string) { + return this.useRequest({ + path: `${API_BASE_PATH}/${encodeURIComponent(name)}`, + method: 'get', + }); + } + + public async createPipeline(pipeline: Pipeline) { + const result = await this.sendRequest({ + path: API_BASE_PATH, + method: 'post', + body: JSON.stringify(pipeline), + }); + + this.trackUiMetric(UIM_PIPELINE_CREATE); + + return result; + } + + public async updatePipeline(pipeline: Pipeline) { + const { name, ...body } = pipeline; + const result = await this.sendRequest({ + path: `${API_BASE_PATH}/${encodeURIComponent(name)}`, + method: 'put', + body: JSON.stringify(body), + }); + + this.trackUiMetric(UIM_PIPELINE_UPDATE); + + return result; + } + + public async deletePipelines(names: string[]) { + const result = this.sendRequest({ + path: `${API_BASE_PATH}/${names.map(name => encodeURIComponent(name)).join(',')}`, + method: 'delete', + }); + + this.trackUiMetric(names.length > 1 ? UIM_PIPELINE_DELETE_MANY : UIM_PIPELINE_DELETE); + + return result; + } + + public async simulatePipeline(testConfig: { + documents: object[]; + verbose?: boolean; + pipeline: Omit; + }) { + const result = await this.sendRequest({ + path: `${API_BASE_PATH}/simulate`, + method: 'post', + body: JSON.stringify(testConfig), + }); + + this.trackUiMetric(UIM_PIPELINE_SIMULATE); + + return result; + } +} + +export const apiService = new ApiService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts new file mode 100644 index 0000000000000..1ccdbbad9b1bb --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.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 { BASE_PATH } from '../../../common/constants'; +import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; + +type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; + +const homeBreadcrumbText = i18n.translate('xpack.ingestPipelines.breadcrumb.pipelinesLabel', { + defaultMessage: 'Ingest Node Pipelines', +}); + +export class BreadcrumbService { + private breadcrumbs: { + [key: string]: Array<{ + text: string; + href?: string; + }>; + } = { + home: [ + { + text: homeBreadcrumbText, + }, + ], + create: [ + { + text: homeBreadcrumbText, + href: `#${BASE_PATH}`, + }, + { + text: i18n.translate('xpack.ingestPipelines.breadcrumb.createPipelineLabel', { + defaultMessage: 'Create pipeline', + }), + }, + ], + edit: [ + { + text: homeBreadcrumbText, + href: `#${BASE_PATH}`, + }, + { + text: i18n.translate('xpack.ingestPipelines.breadcrumb.editPipelineLabel', { + defaultMessage: 'Edit pipeline', + }), + }, + ], + }; + + private setBreadcrumbsHandler?: SetBreadcrumbs; + + public setup(setBreadcrumbsHandler: SetBreadcrumbs): void { + this.setBreadcrumbsHandler = setBreadcrumbsHandler; + } + + public setBreadcrumbs(type: 'create' | 'home' | 'edit'): void { + if (!this.setBreadcrumbsHandler) { + throw new Error('Breadcrumb service has not been initialized'); + } + + const newBreadcrumbs = this.breadcrumbs[type] + ? [...this.breadcrumbs[type]] + : [...this.breadcrumbs.home]; + + this.setBreadcrumbsHandler(newBreadcrumbs); + } +} + +export const breadcrumbService = new BreadcrumbService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts new file mode 100644 index 0000000000000..05fdc4b1dfb84 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts @@ -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. + */ +import { DocLinksStart } from 'src/core/public'; + +export class DocumentationService { + private esDocBasePath: string = ''; + + public setup(docLinks: DocLinksStart): void { + const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; + const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; + + this.esDocBasePath = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; + } + + public getIngestNodeUrl() { + return `${this.esDocBasePath}/ingest.html`; + } + + public getProcessorsUrl() { + return `${this.esDocBasePath}/ingest-processors.html`; + } + + public getHandlingFailureUrl() { + return `${this.esDocBasePath}/handling-failure-in-pipelines.html`; + } + + public getPutPipelineApiUrl() { + return `${this.esDocBasePath}/put-pipeline-api.html`; + } + + public getSimulatePipelineApiUrl() { + return `${this.esDocBasePath}/simulate-pipeline-api.html`; + } +} + +export const documentationService = new DocumentationService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/index.ts b/x-pack/plugins/ingest_pipelines/public/application/services/index.ts new file mode 100644 index 0000000000000..f03a7824f8364 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/index.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. + */ + +export { documentationService, DocumentationService } from './documentation'; + +export { uiMetricService, UiMetricService } from './ui_metric'; + +export { apiService, ApiService } from './api'; + +export { breadcrumbService, BreadcrumbService } from './breadcrumbs'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/ui_metric.ts b/x-pack/plugins/ingest_pipelines/public/application/services/ui_metric.ts new file mode 100644 index 0000000000000..f99bb9ba331d2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/ui_metric.ts @@ -0,0 +1,32 @@ +/* + * 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/public'; + +import { UIM_APP_NAME } from '../constants'; + +export class UiMetricService { + private usageCollection: UsageCollectionSetup | undefined; + + public setup(usageCollection: UsageCollectionSetup) { + this.usageCollection = usageCollection; + } + + private track(name: string) { + if (!this.usageCollection) { + // Usage collection is an optional plugin and might be disabled + return; + } + + const { reportUiStats, METRIC_TYPE } = this.usageCollection; + reportUiStats(UIM_APP_NAME, METRIC_TYPE.COUNT, name); + } + + public trackUiMetric(eventName: string) { + return this.track(eventName); + } +} + +export const uiMetricService = new UiMetricService(); diff --git a/x-pack/plugins/ingest_pipelines/public/index.ts b/x-pack/plugins/ingest_pipelines/public/index.ts new file mode 100644 index 0000000000000..7247973703804 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IngestPipelinesPlugin } from './plugin'; + +export function plugin() { + return new IngestPipelinesPlugin(); +} diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts new file mode 100644 index 0000000000000..e9f5fd6c7f57c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -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. + */ + +import { i18n } from '@kbn/i18n'; +import { CoreSetup, Plugin } from 'src/core/public'; + +import { PLUGIN_ID } from '../common/constants'; +import { uiMetricService, apiService } from './application/services'; +import { Dependencies } from './types'; + +export class IngestPipelinesPlugin implements Plugin { + public setup(coreSetup: CoreSetup, plugins: Dependencies): void { + const { management, usageCollection } = plugins; + const { http } = coreSetup; + + // Initialize services + uiMetricService.setup(usageCollection); + apiService.setup(http, uiMetricService); + + management.sections.getSection('elasticsearch')!.registerApp({ + id: PLUGIN_ID, + title: i18n.translate('xpack.ingestPipelines.appTitle', { + defaultMessage: 'Ingest Node Pipelines', + }), + mount: async params => { + const { mountManagementSection } = await import('./application/mount_management_section'); + + return await mountManagementSection(coreSetup, params); + }, + }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts new file mode 100644 index 0000000000000..cfa946ff942ec --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -0,0 +1,54 @@ +/* + * 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 { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public'; +import { AppServices } from './application'; + +export { + SendRequestConfig, + SendRequestResponse, + UseRequestConfig, + sendRequest, + useRequest, +} from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; + +export { + FormSchema, + FIELD_TYPES, + FormConfig, + useForm, + Form, + getUseField, + ValidationFuncArg, + useFormContext, +} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +export { + fieldFormatters, + fieldValidators, +} from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +export { + getFormRow, + Field, + JsonEditorField, +} from '../../../../src/plugins/es_ui_shared/static/forms/components'; + +export { + isJSON, + isEmptyString, +} from '../../../../src/plugins/es_ui_shared/static/validators/string'; + +export { + SectionLoading, + WithPrivileges, + AuthorizationProvider, + SectionError, + Error, + useAuthorizationContext, + NotAuthorizedSection, +} from '../../../../src/plugins/es_ui_shared/public'; + +export const useKibana = () => _useKibana(); diff --git a/x-pack/plugins/ingest_pipelines/public/types.ts b/x-pack/plugins/ingest_pipelines/public/types.ts new file mode 100644 index 0000000000000..91783ea04fa9a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/types.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 { ManagementSetup } from 'src/plugins/management/public'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; + +export interface Dependencies { + management: ManagementSetup; + usageCollection: UsageCollectionSetup; +} diff --git a/x-pack/plugins/ingest_pipelines/server/index.ts b/x-pack/plugins/ingest_pipelines/server/index.ts new file mode 100644 index 0000000000000..dc162a5d67cb6 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/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 { IngestPipelinesPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new IngestPipelinesPlugin(initializerContext); +} diff --git a/x-pack/plugins/ingest_pipelines/server/lib/index.ts b/x-pack/plugins/ingest_pipelines/server/lib/index.ts new file mode 100644 index 0000000000000..a9a3c61472d8c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { isEsError } from './is_es_error'; diff --git a/x-pack/plugins/ingest_pipelines/server/lib/is_es_error.ts b/x-pack/plugins/ingest_pipelines/server/lib/is_es_error.ts new file mode 100644 index 0000000000000..4137293cf39c0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/lib/is_es_error.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 * as legacyElasticsearch from 'elasticsearch'; + +const esErrorsParent = legacyElasticsearch.errors._Abstract; + +export function isEsError(err: Error) { + return err instanceof esErrorsParent; +} diff --git a/x-pack/plugins/ingest_pipelines/server/plugin.ts b/x-pack/plugins/ingest_pipelines/server/plugin.ts new file mode 100644 index 0000000000000..b27ca417c3e3c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/plugin.ts @@ -0,0 +1,59 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, Plugin, Logger } from 'kibana/server'; + +import { PLUGIN_ID, PLUGIN_MIN_LICENSE_TYPE } from '../common/constants'; + +import { License } from './services'; +import { ApiRoutes } from './routes'; +import { isEsError } from './lib'; +import { Dependencies } from './types'; + +export class IngestPipelinesPlugin implements Plugin { + private readonly logger: Logger; + private readonly license: License; + private readonly apiRoutes: ApiRoutes; + + constructor({ logger }: PluginInitializerContext) { + this.logger = logger.get(); + this.license = new License(); + this.apiRoutes = new ApiRoutes(); + } + + public setup({ http, elasticsearch }: CoreSetup, { licensing }: Dependencies) { + this.logger.debug('ingest_pipelines: setup'); + + const router = http.createRouter(); + + this.license.setup( + { + pluginId: PLUGIN_ID, + minimumLicenseType: PLUGIN_MIN_LICENSE_TYPE, + defaultErrorMessage: i18n.translate('xpack.ingestPipelines.licenseCheckErrorMessage', { + defaultMessage: 'License check failed', + }), + }, + { + licensing, + logger: this.logger, + } + ); + + this.apiRoutes.setup({ + router, + license: this.license, + lib: { + isEsError, + }, + }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts new file mode 100644 index 0000000000000..63637eaac765d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -0,0 +1,83 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +import { Pipeline } from '../../../common/types'; +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const bodySchema = schema.object({ + name: schema.string(), + description: schema.string(), + processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + version: schema.maybe(schema.number()), + on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), +}); + +export const registerCreateRoute = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.post( + { + path: API_BASE_PATH, + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const pipeline = req.body as Pipeline; + + const { name, description, processors, version, on_failure } = pipeline; + + try { + // Check that a pipeline with the same name doesn't already exist + const pipelineByName = await callAsCurrentUser('ingest.getPipeline', { id: name }); + + if (pipelineByName[name]) { + return res.conflict({ + body: new Error( + i18n.translate('xpack.ingestPipelines.createRoute.duplicatePipelineIdErrorMessage', { + defaultMessage: "There is already a pipeline with name '{name}'.", + values: { + name, + }, + }) + ), + }); + } + } catch (e) { + // Silently swallow error + } + + try { + const response = await callAsCurrentUser('ingest.putPipeline', { + id: name, + body: { + description, + processors, + version, + on_failure, + }, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts new file mode 100644 index 0000000000000..4664b49a08a50 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts @@ -0,0 +1,49 @@ +/* + * 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 { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const paramsSchema = schema.object({ + names: schema.string(), +}); + +export const registerDeleteRoute = ({ router, license }: RouteDependencies): void => { + router.delete( + { + path: `${API_BASE_PATH}/{names}`, + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const { names } = req.params; + const pipelineNames = names.split(','); + + const response: { itemsDeleted: string[]; errors: any[] } = { + itemsDeleted: [], + errors: [], + }; + + await Promise.all( + pipelineNames.map(pipelineName => { + return callAsCurrentUser('ingest.deletePipeline', { id: pipelineName }) + .then(() => response.itemsDeleted.push(pipelineName)) + .catch(e => + response.errors.push({ + name: pipelineName, + error: e, + }) + ); + }) + ); + + return res.ok({ body: response }); + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts new file mode 100644 index 0000000000000..ec92262014272 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts @@ -0,0 +1,83 @@ +/* + * 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 { deserializePipelines } from '../../../common/lib'; +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const paramsSchema = schema.object({ + name: schema.string(), +}); + +export const registerGetRoutes = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + // Get all pipelines + router.get( + { path: API_BASE_PATH, validate: false }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + + try { + const pipelines = await callAsCurrentUser('ingest.getPipeline'); + + return res.ok({ body: deserializePipelines(pipelines) }); + } catch (error) { + if (isEsError(error)) { + // ES returns 404 when there are no pipelines + // Instead, we return an empty array and 200 status back to the client + if (error.status === 404) { + return res.ok({ body: [] }); + } + + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); + + // Get single pipeline + router.get( + { + path: `${API_BASE_PATH}/{name}`, + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const { name } = req.params; + + try { + const pipeline = await callAsCurrentUser('ingest.getPipeline', { id: name }); + + return res.ok({ + body: { + ...pipeline[name], + name, + }, + }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts new file mode 100644 index 0000000000000..58a4bf5617659 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/index.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. + */ + +export { registerGetRoutes } from './get'; + +export { registerCreateRoute } from './create'; + +export { registerUpdateRoute } from './update'; + +export { registerPrivilegesRoute } from './privileges'; + +export { registerDeleteRoute } from './delete'; + +export { registerSimulateRoute } from './simulate'; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts new file mode 100644 index 0000000000000..2e1c11928959f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts @@ -0,0 +1,62 @@ +/* + * 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 { RouteDependencies } from '../../types'; +import { API_BASE_PATH, APP_CLUSTER_REQUIRED_PRIVILEGES } from '../../../common/constants'; +import { Privileges } from '../../../../../../src/plugins/es_ui_shared/public'; + +const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } = {}): string[] => + Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { + if (!privilegesObject[privilegeName]) { + privileges.push(privilegeName); + } + return privileges; + }, []); + +export const registerPrivilegesRoute = ({ license, router }: RouteDependencies) => { + router.get( + { + path: `${API_BASE_PATH}/privileges`, + validate: false, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { + core: { + elasticsearch: { dataClient }, + }, + } = ctx; + + const privilegesResult: Privileges = { + hasAllPrivileges: true, + missingPrivileges: { + cluster: [], + }, + }; + + try { + const { has_all_requested: hasAllPrivileges, cluster } = await dataClient.callAsCurrentUser( + 'transport.request', + { + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + cluster: APP_CLUSTER_REQUIRED_PRIVILEGES, + }, + } + ); + + if (!hasAllPrivileges) { + privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); + } + + privilegesResult.hasAllPrivileges = hasAllPrivileges; + + return res.ok({ body: privilegesResult }); + } catch (e) { + return res.internalError(e); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts new file mode 100644 index 0000000000000..ca5fc78d118fd --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts @@ -0,0 +1,61 @@ +/* + * 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 { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const bodySchema = schema.object({ + pipeline: schema.object({ + description: schema.string(), + processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + version: schema.maybe(schema.number()), + on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), + }), + documents: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + verbose: schema.maybe(schema.boolean()), +}); + +export const registerSimulateRoute = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.post( + { + path: `${API_BASE_PATH}/simulate`, + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + + const { pipeline, documents, verbose } = req.body; + + try { + const response = await callAsCurrentUser('ingest.simulate', { + verbose, + body: { + pipeline, + docs: documents, + }, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts new file mode 100644 index 0000000000000..a6fdee47f0ecf --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const bodySchema = schema.object({ + description: schema.string(), + processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + version: schema.maybe(schema.number()), + on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), +}); + +const paramsSchema = schema.object({ + name: schema.string(), +}); + +export const registerUpdateRoute = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.put( + { + path: `${API_BASE_PATH}/{name}`, + validate: { + body: bodySchema, + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const { name } = req.params; + const { description, processors, version, on_failure } = req.body; + + try { + // Verify pipeline exists; ES will throw 404 if it doesn't + await callAsCurrentUser('ingest.getPipeline', { id: name }); + + const response = await callAsCurrentUser('ingest.putPipeline', { + id: name, + body: { + description, + processors, + version, + on_failure, + }, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/index.ts new file mode 100644 index 0000000000000..f703a460143f4 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/index.ts @@ -0,0 +1,27 @@ +/* + * 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 { RouteDependencies } from '../types'; + +import { + registerGetRoutes, + registerCreateRoute, + registerUpdateRoute, + registerPrivilegesRoute, + registerDeleteRoute, + registerSimulateRoute, +} from './api'; + +export class ApiRoutes { + setup(dependencies: RouteDependencies) { + registerGetRoutes(dependencies); + registerCreateRoute(dependencies); + registerUpdateRoute(dependencies); + registerPrivilegesRoute(dependencies); + registerDeleteRoute(dependencies); + registerSimulateRoute(dependencies); + } +} diff --git a/x-pack/plugins/ingest_pipelines/server/services/index.ts b/x-pack/plugins/ingest_pipelines/server/services/index.ts new file mode 100644 index 0000000000000..b7a45e59549eb --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/services/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { License } from './license'; diff --git a/x-pack/plugins/ingest_pipelines/server/services/license.ts b/x-pack/plugins/ingest_pipelines/server/services/license.ts new file mode 100644 index 0000000000000..0a4748bd0ace0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/services/license.ts @@ -0,0 +1,82 @@ +/* + * 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 { Logger } from 'src/core/server'; +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'kibana/server'; + +import { LicensingPluginSetup } from '../../../licensing/server'; +import { LicenseType } from '../../../licensing/common/types'; + +export interface LicenseStatus { + isValid: boolean; + message?: string; +} + +interface SetupSettings { + pluginId: string; + minimumLicenseType: LicenseType; + defaultErrorMessage: string; +} + +export class License { + private licenseStatus: LicenseStatus = { + isValid: false, + message: 'Invalid License', + }; + + setup( + { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, + { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } + ) { + licensing.license$.subscribe(license => { + const { state, message } = license.check(pluginId, minimumLicenseType); + const hasRequiredLicense = state === 'valid'; + + if (hasRequiredLicense) { + this.licenseStatus = { isValid: true }; + } else { + this.licenseStatus = { + isValid: false, + message: message || defaultErrorMessage, + }; + if (message) { + logger.info(message); + } + } + }); + } + + guardApiRoute(handler: RequestHandler) { + const license = this; + + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseStatus = license.getStatus(); + + if (!licenseStatus.isValid) { + return response.customError({ + body: { + message: licenseStatus.message || '', + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; + } + + getStatus() { + return this.licenseStatus; + } +} diff --git a/x-pack/plugins/ingest_pipelines/server/types.ts b/x-pack/plugins/ingest_pipelines/server/types.ts new file mode 100644 index 0000000000000..0135ae8e2f07d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'src/core/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { License } from './services'; +import { isEsError } from './lib'; + +export interface Dependencies { + licensing: LicensingPluginSetup; +} + +export interface RouteDependencies { + router: IRouter; + license: License; + lib: { + isEsError: typeof isEsError; + }; +} diff --git a/x-pack/test/api_integration/apis/management/index.js b/x-pack/test/api_integration/apis/management/index.js index 352cd56d0fc9f..cef2caa918620 100644 --- a/x-pack/test/api_integration/apis/management/index.js +++ b/x-pack/test/api_integration/apis/management/index.js @@ -12,5 +12,6 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./rollup')); loadTestFile(require.resolve('./index_management')); loadTestFile(require.resolve('./index_lifecycle_management')); + loadTestFile(require.resolve('./ingest_pipelines')); }); } diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts new file mode 100644 index 0000000000000..ca222ebc2c1e3 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('Ingest Node Pipelines', () => { + loadTestFile(require.resolve('./ingest_pipelines')); + }); +} diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts new file mode 100644 index 0000000000000..88a78d048a3b6 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -0,0 +1,330 @@ +/* + * 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 expect from '@kbn/expect'; +import { registerEsHelpers } from './lib'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const API_BASE_PATH = '/api/ingest_pipelines'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const { createPipeline, deletePipeline } = registerEsHelpers(getService); + + describe('Pipelines', function() { + describe('Create', () => { + const PIPELINE_ID = 'test_create_pipeline'; + after(() => deletePipeline(PIPELINE_ID)); + + it('should create a pipeline', async () => { + const { body } = await supertest + .post(API_BASE_PATH) + .set('kbn-xsrf', 'xxx') + .send({ + name: PIPELINE_ID, + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + on_failure: [ + { + set: { + field: 'error.message', + value: '{{ failure_message }}', + }, + }, + ], + version: 1, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow creation of an existing pipeline', async () => { + const { body } = await supertest + .post(API_BASE_PATH) + .set('kbn-xsrf', 'xxx') + .send({ + name: PIPELINE_ID, + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }) + .expect(409); + + expect(body).to.eql({ + statusCode: 409, + error: 'Conflict', + message: `There is already a pipeline with name '${PIPELINE_ID}'.`, + }); + }); + }); + + describe('Update', () => { + const PIPELINE_ID = 'test_update_pipeline'; + const PIPELINE = { + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }; + + before(() => createPipeline({ body: PIPELINE, id: PIPELINE_ID })); + after(() => deletePipeline(PIPELINE_ID)); + + it('should allow an existing pipeline to be updated', async () => { + const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...PIPELINE, + description: 'updated test pipeline description', + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow a non-existing pipeline to be updated', async () => { + const uri = `${API_BASE_PATH}/pipeline_does_not_exist`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...PIPELINE, + description: 'updated test pipeline description', + }) + .expect(404); + + expect(body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + }); + }); + + describe('Get', () => { + const PIPELINE_ID = 'test_pipeline'; + const PIPELINE = { + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }; + + before(() => createPipeline({ body: PIPELINE, id: PIPELINE_ID })); + after(() => deletePipeline(PIPELINE_ID)); + + describe('all pipelines', () => { + it('should return an array of pipelines', async () => { + const { body } = await supertest + .get(API_BASE_PATH) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(Array.isArray(body)).to.be(true); + + // There are some pipelines created OOTB with ES + // To not be dependent on these, we only confirm the pipeline we created as part of the test exists + const testPipeline = body.find(({ name }: { name: string }) => name === PIPELINE_ID); + + expect(testPipeline).to.eql({ + ...PIPELINE, + name: PIPELINE_ID, + }); + }); + }); + + describe('one pipeline', () => { + it('should return a single pipeline', async () => { + const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + + const { body } = await supertest + .get(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.eql({ + ...PIPELINE, + name: PIPELINE_ID, + }); + }); + }); + }); + + describe('Delete', () => { + const PIPELINE = { + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }; + + it('should delete a pipeline', async () => { + // Create pipeline to be deleted + const PIPELINE_ID = 'test_delete_pipeline'; + createPipeline({ body: PIPELINE, id: PIPELINE_ID }); + + const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + + const { body } = await supertest + .delete(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.eql({ + itemsDeleted: [PIPELINE_ID], + errors: [], + }); + }); + + it('should delete multiple pipelines', async () => { + // Create pipelines to be deleted + const PIPELINE_ONE_ID = 'test_delete_pipeline_1'; + const PIPELINE_TWO_ID = 'test_delete_pipeline_2'; + createPipeline({ body: PIPELINE, id: PIPELINE_ONE_ID }); + createPipeline({ body: PIPELINE, id: PIPELINE_TWO_ID }); + + const uri = `${API_BASE_PATH}/${PIPELINE_ONE_ID},${PIPELINE_TWO_ID}`; + + const { + body: { itemsDeleted, errors }, + } = await supertest + .delete(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(errors).to.eql([]); + + // The itemsDeleted array order isn't guaranteed, so we assert against each pipeline name instead + [PIPELINE_ONE_ID, PIPELINE_TWO_ID].forEach(pipelineName => { + expect(itemsDeleted.includes(pipelineName)).to.be(true); + }); + }); + + it('should return an error for any pipelines not sucessfully deleted', async () => { + const PIPELINE_DOES_NOT_EXIST = 'pipeline_does_not_exist'; + + // Create pipeline to be deleted + const PIPELINE_ONE_ID = 'test_delete_pipeline_1'; + createPipeline({ body: PIPELINE, id: PIPELINE_ONE_ID }); + + const uri = `${API_BASE_PATH}/${PIPELINE_ONE_ID},${PIPELINE_DOES_NOT_EXIST}`; + + const { body } = await supertest + .delete(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.eql({ + itemsDeleted: [PIPELINE_ONE_ID], + errors: [ + { + name: PIPELINE_DOES_NOT_EXIST, + error: { + msg: '[resource_not_found_exception] pipeline [pipeline_does_not_exist] is missing', + path: '/_ingest/pipeline/pipeline_does_not_exist', + query: {}, + statusCode: 404, + response: JSON.stringify({ + error: { + root_cause: [ + { + type: 'resource_not_found_exception', + reason: 'pipeline [pipeline_does_not_exist] is missing', + }, + ], + type: 'resource_not_found_exception', + reason: 'pipeline [pipeline_does_not_exist] is missing', + }, + status: 404, + }), + }, + }, + ], + }); + }); + }); + + describe('Simulate', () => { + it('should successfully simulate a pipeline', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/simulate`) + .set('kbn-xsrf', 'xxx') + .send({ + pipeline: { + description: 'test simulate pipeline description', + processors: [ + { + set: { + field: 'field2', + value: '_value', + }, + }, + ], + }, + documents: [ + { + _index: 'index', + _id: 'id', + _source: { + foo: 'bar', + }, + }, + { + _index: 'index', + _id: 'id', + _source: { + foo: 'rab', + }, + }, + ], + }) + .expect(200); + + // The simulate ES response is quite long and includes timestamps + // so for now, we just confirm the docs array is returned with the correct length + expect(body.docs?.length).to.eql(2); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts new file mode 100644 index 0000000000000..2f42596a66b54 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts @@ -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. + */ +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +interface Processor { + [key: string]: { + [key: string]: unknown; + }; +} + +interface Pipeline { + id: string; + body: { + description: string; + processors: Processor[]; + version?: number; + }; +} + +/** + * Helpers to create and delete pipelines on the Elasticsearch instance + * during our tests. + * @param {ElasticsearchClient} es The Elasticsearch client instance + */ +export const registerEsHelpers = (getService: FtrProviderContext['getService']) => { + const es = getService('legacyEs'); + + const createPipeline = (pipeline: Pipeline) => es.ingest.putPipeline(pipeline); + + const deletePipeline = (pipelineId: string) => es.ingest.deletePipeline({ id: pipelineId }); + + return { + createPipeline, + deletePipeline, + }; +}; diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts new file mode 100644 index 0000000000000..66ea0fe40c4ce --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { registerEsHelpers } from './elasticsearch'; diff --git a/x-pack/test/functional/apps/ingest_pipelines/index.ts b/x-pack/test/functional/apps/ingest_pipelines/index.ts new file mode 100644 index 0000000000000..87e7e70c0b5e0 --- /dev/null +++ b/x-pack/test/functional/apps/ingest_pipelines/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext) => { + describe('Ingest pipelines app', function() { + this.tags('ciGroup3'); + loadTestFile(require.resolve('./ingest_pipelines')); + }); +}; diff --git a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts new file mode 100644 index 0000000000000..1b22f8f35d7ad --- /dev/null +++ b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts @@ -0,0 +1,27 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'ingestPipelines']); + const log = getService('log'); + + describe('Ingest Pipelines', function() { + this.tags('smoke'); + before(async () => { + await pageObjects.common.navigateToApp('ingestPipelines'); + }); + + it('Loads the app', async () => { + await log.debug('Checking for section heading to say Ingest Node Pipelines.'); + + const headingText = await pageObjects.ingestPipelines.sectionHeadingText(); + expect(headingText).to.be('Ingest Node Pipelines'); + }); + }); +}; diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 2c6238704bea0..f6b80b1b9fc67 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -53,6 +53,7 @@ export default async function({ readConfigFile }) { resolve(__dirname, './apps/index_patterns'), resolve(__dirname, './apps/index_management'), resolve(__dirname, './apps/index_lifecycle_management'), + resolve(__dirname, './apps/ingest_pipelines'), resolve(__dirname, './apps/snapshot_restore'), resolve(__dirname, './apps/cross_cluster_replication'), resolve(__dirname, './apps/remote_clusters'), @@ -175,6 +176,10 @@ export default async function({ readConfigFile }) { pathname: '/app/kibana', hash: '/management/elasticsearch/index_lifecycle_management', }, + ingestPipelines: { + pathname: '/app/kibana', + hash: '/management/elasticsearch/ingest_pipelines', + }, snapshotRestore: { pathname: '/app/kibana', hash: '/management/elasticsearch/snapshot_restore', diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 4b8c2944ef190..833cc452a5d31 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -45,6 +45,7 @@ import { LensPageProvider } from './lens_page'; import { InfraMetricExplorerProvider } from './infra_metric_explorer'; import { RoleMappingsPageProvider } from './role_mappings_page'; import { SpaceSelectorPageProvider } from './space_selector_page'; +import { IngestPipelinesPageProvider } from './ingest_pipelines_page'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -78,4 +79,5 @@ export const pageObjects = { copySavedObjectsToSpace: CopySavedObjectsToSpacePageProvider, lens: LensPageProvider, roleMappings: RoleMappingsPageProvider, + ingestPipelines: IngestPipelinesPageProvider, }; diff --git a/x-pack/test/functional/page_objects/ingest_pipelines_page.ts b/x-pack/test/functional/page_objects/ingest_pipelines_page.ts new file mode 100644 index 0000000000000..abc85277a3617 --- /dev/null +++ b/x-pack/test/functional/page_objects/ingest_pipelines_page.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 { FtrProviderContext } from '../ftr_provider_context'; + +export function IngestPipelinesPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async sectionHeadingText() { + return await testSubjects.getVisibleText('appTitle'); + }, + }; +} From 59315bc84d3fbf8bde3b7bebdf0308b1749ed978 Mon Sep 17 00:00:00 2001 From: igoristic Date: Thu, 30 Apr 2020 15:59:35 -0400 Subject: [PATCH 051/122] [Monitoring] NP Migration complete client cutover (#62908) * Final phase before the complete cutover * NP migration * lint fix * More NP stuff * Moved Stack Monitoring client plugin outside legacy and fixed all tests * ... * Removed unused files * Fix for main links * Fixed more tests * Fixed redirect when clicking on SM icon again * Code review feedback * Addressed code review feedback * Fixed return value --- .eslintignore | 1 + .eslintrc.js | 6 + .github/CODEOWNERS | 1 + src/dev/precommit_hook/casing_check_config.js | 12 +- x-pack/legacy/plugins/monitoring/.agignore | 3 - .../__tests__/format_timestamp_to_duration.js | 128 - .../monitoring/common/cancel_promise.ts | 70 - .../plugins/monitoring/common/constants.ts | 268 -- .../common/format_timestamp_to_duration.js | 54 - .../plugins/monitoring/common/formatting.js | 35 - .../monitoring/{config.js => config.ts} | 2 +- x-pack/legacy/plugins/monitoring/index.js | 57 - x-pack/legacy/plugins/monitoring/index.ts | 41 + .../components/chart/chart_target.test.js | 76 - .../public/components/license/index.js | 91 - .../monitoring/public/directives/all.js | 10 - .../monitoring/public/filters/index.js | 30 - .../hacks/__tests__/toggle_app_link_in_nav.js | 9 - .../public/hacks/toggle_app_link_in_nav.js | 16 - .../monitoring/public/icons/monitoring.svg | 16 - .../plugins/monitoring/public/legacy.ts | 27 - .../np_imports/angular/angular_config.ts | 157 - .../public/np_imports/angular/index.ts | 48 - .../public/np_imports/angular/modules.ts | 148 - .../np_imports/angular/providers/promises.js | 116 - .../public/np_imports/legacy_imports.ts | 22 - .../public/np_imports/ui/capabilities.ts | 8 - .../monitoring/public/np_imports/ui/chrome.ts | 33 - .../public/np_imports/ui/modules.ts | 55 - .../public/np_imports/ui/timefilter.ts | 31 - .../monitoring/public/np_ready/plugin.ts | 28 - .../monitoring/public/register_feature.ts | 30 - .../monitoring/public/services/breadcrumbs.js | 10 - .../monitoring/public/services/executor.js | 10 - .../public/views/kibana/instance/index.js | 153 - .../public/views/loading/index.html | 5 - .../monitoring/public/views/loading/index.js | 51 - .../legacy/plugins/monitoring/ui_exports.js | 65 - .../server/routes/api/v1/settings.js | 2 +- x-pack/plugins/monitoring/kibana.json | 6 +- .../plugins/monitoring/public/_hacks.scss | 0 .../monitoring/public/angular/app_modules.ts | 248 ++ .../public/angular/helpers}/routes.ts | 19 +- .../public/angular/helpers}/utils.ts | 0 .../monitoring/public/angular/index.ts | 68 + .../public}/angular/providers/private.js | 2 + .../public/angular/providers/url.js | 217 ++ .../alerts/__snapshots__/status.test.tsx.snap | 0 .../alerts/__tests__/map_severity.js | 0 .../public/components/alerts/alerts.js | 8 +- .../__snapshots__/configuration.test.tsx.snap | 0 .../__snapshots__/step1.test.tsx.snap | 0 .../__snapshots__/step2.test.tsx.snap | 0 .../__snapshots__/step3.test.tsx.snap | 0 .../configuration/configuration.test.tsx | 16 +- .../alerts/configuration/configuration.tsx | 8 +- .../components/alerts/configuration/index.ts | 0 .../alerts/configuration/step1.test.tsx | 84 +- .../components/alerts/configuration/step1.tsx | 12 +- .../alerts/configuration/step2.test.tsx | 0 .../components/alerts/configuration/step2.tsx | 0 .../alerts/configuration/step3.test.tsx | 0 .../components/alerts/configuration/step3.tsx | 0 .../components/alerts/formatted_alert.js | 0 .../public/components/alerts/index.js | 0 .../components/alerts/manage_email_action.tsx | 2 +- .../public/components/alerts/map_severity.js | 0 .../public/components/alerts/status.test.tsx | 18 +- .../public/components/alerts/status.tsx | 16 +- .../public/components/apm/instance/index.js | 0 .../components/apm/instance/instance.js | 0 .../public/components/apm/instance/status.js | 0 .../public/components/apm/instances/index.js | 0 .../components/apm/instances/instances.js | 0 .../public/components/apm/instances/status.js | 0 .../public/components/apm/overview/index.js | 0 .../public/components/apm/status_icon.js | 2 +- .../public/components/beats/beat/beat.js | 0 .../public/components/beats/beat/index.js | 0 .../public/components/beats/index.js | 0 .../public/components/beats/listing/index.js | 0 .../components/beats/listing/listing.js | 6 +- .../__snapshots__/latest_active.test.js.snap | 0 .../__snapshots__/latest_types.test.js.snap | 0 .../latest_versions.test.js.snap | 0 .../__snapshots__/overview.test.js.snap | 0 .../public/components/beats/overview/index.js | 0 .../beats/overview/latest_active.js | 0 .../beats/overview/latest_active.test.js | 0 .../components/beats/overview/latest_types.js | 0 .../beats/overview/latest_types.test.js | 0 .../beats/overview/latest_versions.js | 0 .../beats/overview/latest_versions.test.js | 0 .../components/beats/overview/overview.js | 0 .../beats/overview/overview.test.js | 8 +- .../public/components/beats/stats.js | 2 +- .../components/chart/__tests__/get_color.js | 0 .../chart/__tests__/get_last_value.js | 0 .../components/chart/__tests__/get_title.js | 0 .../chart/__tests__/get_values_for_legend.js | 0 .../public/components/chart/_chart.scss | 0 .../public/components/chart/_index.scss | 0 .../public/components/chart/chart_target.js | 2 +- .../public/components/chart/event_bus.js | 0 .../components/chart/get_chart_options.js | 4 +- .../public/components/chart/get_color.js | 0 .../public/components/chart/get_last_value.js | 0 .../public/components/chart/get_title.js | 0 .../public/components/chart/get_units.js | 0 .../components/chart/get_values_for_legend.js | 0 .../components/chart/horizontal_legend.js | 0 .../public/components/chart/index.js | 0 .../public/components/chart/info_tooltip.js | 0 .../components/chart/monitoring_timeseries.js | 0 .../chart/monitoring_timeseries_container.js | 0 .../components/chart/timeseries_container.js | 0 .../chart/timeseries_visualization.js | 0 .../cluster/listing/alerts_indicator.js | 2 +- .../components/cluster/listing/index.js | 0 .../components/cluster/listing/listing.js | 19 +- .../__snapshots__/helpers.test.js.snap | 0 .../overview/__tests__/helpers.test.js | 2 +- .../cluster/overview/alerts_panel.js | 4 +- .../components/cluster/overview/apm_panel.js | 2 +- .../cluster/overview/beats_panel.js | 2 +- .../cluster/overview/elasticsearch_panel.js | 2 +- .../components/cluster/overview/helpers.js | 0 .../components/cluster/overview/index.js | 0 .../cluster/overview/kibana_panel.js | 2 +- .../cluster/overview/license_text.js | 0 .../cluster/overview/logstash_panel.js | 2 +- .../public/components/cluster/status_icon.js | 0 .../ccr/__snapshots__/ccr.test.js.snap | 0 .../components/elasticsearch/ccr/ccr.js | 3 +- .../components/elasticsearch/ccr/ccr.test.js | 0 .../components/elasticsearch/ccr/index.js | 0 .../components/elasticsearch/ccr/index.scss} | 0 .../__snapshots__/ccr_shard.test.js.snap | 0 .../elasticsearch/ccr_shard/ccr_shard.js | 4 +- .../elasticsearch/ccr_shard/ccr_shard.test.js | 11 +- .../elasticsearch/ccr_shard/index.js | 0 .../elasticsearch/ccr_shard/status.js | 0 .../elasticsearch/cluster_status/index.js | 0 .../public/components/elasticsearch/index.js | 0 .../elasticsearch/index/advanced.js | 0 .../components/elasticsearch/index/index.js | 0 .../index_detail_status/index.js | 0 .../components/elasticsearch/indices/index.js | 0 .../elasticsearch/indices/indices.js | 0 .../ml_job_listing/status_icon.js | 2 +- .../components/elasticsearch/node/advanced.js | 0 .../components/elasticsearch/node/index.js | 0 .../components/elasticsearch/node/node.js | 0 .../elasticsearch/node/status_icon.js | 0 .../elasticsearch/node_detail_status/index.js | 0 .../__snapshots__/cells.test.js.snap | 0 .../nodes/__tests__/cells.test.js | 2 +- .../components/elasticsearch/nodes/cells.js | 0 .../components/elasticsearch/nodes/index.js | 0 .../components/elasticsearch/nodes/nodes.js | 0 .../elasticsearch/overview/index.js | 0 .../elasticsearch/overview/overview.js | 0 .../elasticsearch/shard_activity/index.js | 0 .../shard_activity/parse_props.js | 6 +- .../elasticsearch/shard_activity/progress.js | 0 .../shard_activity/recovery_index.js | 0 .../shard_activity/shard_activity.js | 2 +- .../elasticsearch/shard_activity/snapshot.js | 0 .../shard_activity/source_destination.js | 0 .../shard_activity/source_tooltip.js | 0 .../shard_activity/total_time.js | 0 .../shard_allocation/_index.scss | 0 .../shard_allocation/_shard_allocation.scss | 0 .../__snapshots__/shard.test.js.snap | 0 .../shard_allocation/components/assigned.js | 0 .../components/cluster_view.js | 0 .../shard_allocation/components/shard.js | 0 .../shard_allocation/components/shard.test.js | 0 .../shard_allocation/components/table_body.js | 0 .../shard_allocation/components/table_head.js | 0 .../shard_allocation/components/unassigned.js | 0 .../elasticsearch/shard_allocation/index.js | 0 .../shard_allocation/lib/calculate_class.js | 0 .../shard_allocation/lib/decorate_shards.js | 0 .../lib/decorate_shards.test.js | 0 .../lib/generate_query_and_link.js | 0 .../lib/has_primary_children.js | 0 .../shard_allocation/lib/has_unassigned.js | 0 .../shard_allocation/lib/labels.js | 0 .../shard_allocation/lib/vents.js | 0 .../shard_allocation/shard_allocation.js | 0 .../transformers/indices_by_nodes.js | 0 .../transformers/nodes_by_indices.js | 0 .../components/elasticsearch/status_icon.js | 0 .../monitoring/public/components/index.js | 0 .../components/kibana/cluster_status/index.js | 0 .../components/kibana/detail_status/index.js | 0 .../components/kibana/instances/index.js | 0 .../components/kibana/instances/instances.js | 2 +- .../public/components/kibana/status_icon.js | 2 +- .../public/components/license/index.js | 201 ++ .../logs/__snapshots__/logs.test.js.snap | 0 .../logs/__snapshots__/reason.test.js.snap | 0 .../public/components/logs/index.js | 0 .../monitoring/public/components/logs/logs.js | 9 +- .../public/components/logs/logs.test.js | 23 +- .../public/components/logs/reason.js | 3 +- .../public/components/logs/reason.test.js | 12 +- .../logstash/cluster_status/index.js | 0 .../logstash/detail_status/index.js | 0 .../__snapshots__/listing.test.js.snap | 0 .../components/logstash/listing/index.js | 0 .../components/logstash/listing/listing.js | 0 .../logstash/listing/listing.test.js | 0 .../components/logstash/overview/index.js | 0 .../components/logstash/overview/overview.js | 0 .../logstash/pipeline_listing/index.js | 0 .../pipeline_listing/pipeline_listing.js | 2 +- .../logstash/pipeline_viewer/index.js | 0 .../models/__tests__/config.js | 0 .../models/__tests__/pipeline_state.js | 0 .../logstash/pipeline_viewer/models/config.js | 0 .../models/graph/__tests__/boolean_edge.js | 0 .../models/graph/__tests__/edge.js | 0 .../models/graph/__tests__/edge_factory.js | 0 .../models/graph/__tests__/if_vertex.js | 0 .../models/graph/__tests__/index.js | 0 .../models/graph/__tests__/plugin_vertex.js | 0 .../models/graph/__tests__/queue_vertex.js | 0 .../models/graph/__tests__/vertex.js | 0 .../models/graph/__tests__/vertex_factory.js | 0 .../models/graph/boolean_edge.js | 0 .../pipeline_viewer/models/graph/edge.js | 0 .../models/graph/edge_factory.js | 0 .../pipeline_viewer/models/graph/if_vertex.js | 0 .../pipeline_viewer/models/graph/index.js | 0 .../models/graph/plugin_vertex.js | 0 .../models/graph/queue_vertex.js | 0 .../pipeline_viewer/models/graph/vertex.js | 0 .../models/graph/vertex_factory.js | 0 .../pipeline_viewer/models/list/element.js | 0 .../models/list/else_element.js | 0 .../models/list/else_element.test.js | 0 .../models/list/flatten_pipeline_section.js | 0 .../list/flatten_pipeline_section.test.js | 0 .../pipeline_viewer/models/list/if_element.js | 0 .../models/list/if_element.test.js | 0 .../pipeline_viewer/models/list/index.js | 0 .../pipeline_viewer/models/list/list.js | 0 .../pipeline_viewer/models/list/list.test.js | 0 .../models/list/plugin_element.js | 0 .../models/list/plugin_element.test.js | 0 .../models/pipeline/__tests__/if_statement.js | 0 .../pipeline/__tests__/make_statement.js | 0 .../models/pipeline/__tests__/pipeline.js | 0 .../pipeline/__tests__/plugin_statement.js | 0 .../models/pipeline/__tests__/queue.js | 0 .../models/pipeline/__tests__/statement.js | 0 .../models/pipeline/__tests__/utils.js | 0 .../models/pipeline/if_statement.js | 0 .../pipeline_viewer/models/pipeline/index.js | 0 .../models/pipeline/make_statement.js | 0 .../models/pipeline/pipeline.js | 0 .../models/pipeline/plugin_statement.js | 0 .../pipeline_viewer/models/pipeline/queue.js | 0 .../models/pipeline/statement.js | 0 .../pipeline_viewer/models/pipeline/utils.js | 0 .../pipeline_viewer/models/pipeline_state.js | 0 .../collapsible_statement.test.js.snap | 0 .../__snapshots__/detail_drawer.test.js.snap | 0 .../__snapshots__/metric.test.js.snap | 0 .../pipeline_viewer.test.js.snap | 0 .../plugin_statement.test.js.snap | 0 .../__test__/__snapshots__/queue.test.js.snap | 0 .../__snapshots__/statement.test.js.snap | 0 .../__snapshots__/statement_list.test.js.snap | 0 .../statement_list_heading.test.js.snap | 0 .../statement_section.test.js.snap | 0 .../__test__/collapsible_statement.test.js | 0 .../views/__test__/detail_drawer.test.js | 4 + .../views/__test__/metric.test.js | 0 .../views/__test__/pipeline_viewer.test.js | 4 + .../views/__test__/plugin_statement.test.js | 0 .../views/__test__/queue.test.js | 0 .../views/__test__/statement.test.js | 0 .../views/__test__/statement_list.test.js | 0 .../__test__/statement_list_heading.test.js | 0 .../views/__test__/statement_section.test.js | 0 .../pipeline_viewer/views/_index.scss | 0 .../views/_pipeline_viewer.scss | 0 .../views/collapsible_statement.js | 0 .../pipeline_viewer/views/detail_drawer.js | 0 .../logstash/pipeline_viewer/views/index.js | 0 .../logstash/pipeline_viewer/views/metric.js | 0 .../pipeline_viewer/views/pipeline_viewer.js | 0 .../pipeline_viewer/views/plugin_statement.js | 0 .../logstash/pipeline_viewer/views/queue.js | 0 .../pipeline_viewer/views/statement.js | 0 .../pipeline_viewer/views/statement_list.js | 0 .../views/statement_list_heading.js | 0 .../views/statement_section.js | 0 .../metricbeat_migration/constants.js | 0 .../flyout/__snapshots__/flyout.test.js.snap | 0 .../metricbeat_migration/flyout/flyout.js | 5 +- .../flyout/flyout.test.js | 13 +- .../metricbeat_migration/flyout/index.js | 0 ...isable_internal_collection_instructions.js | 0 .../apm/enable_metricbeat_instructions.js | 3 +- .../instruction_steps/apm/index.js | 0 .../beats/common_beats_instructions.js | 0 ...isable_internal_collection_instructions.js | 0 .../beats/enable_metricbeat_instructions.js | 3 +- .../instruction_steps/beats/index.js | 0 .../instruction_steps/common_instructions.js | 0 .../components/monospace/index.js | 0 .../components/monospace/monospace.js | 0 ...isable_internal_collection_instructions.js | 0 .../enable_metricbeat_instructions.js | 3 +- .../instruction_steps/elasticsearch/index.js | 0 .../get_instruction_steps.js | 0 .../instruction_steps/index.js | 0 ...isable_internal_collection_instructions.js | 0 .../kibana/enable_metricbeat_instructions.js | 3 +- .../instruction_steps/kibana/index.js | 0 ...isable_internal_collection_instructions.js | 0 .../enable_metricbeat_instructions.js | 3 +- .../instruction_steps/logstash/index.js | 0 .../__snapshots__/checker_errors.test.js.snap | 0 .../__snapshots__/no_data.test.js.snap | 0 .../no_data/__tests__/checker_errors.test.js | 2 +- .../no_data/__tests__/no_data.test.js | 8 +- .../public/components/no_data/_index.scss | 0 .../public/components/no_data/_no_data.scss | 0 .../no_data/blurbs/changes_needed.js | 0 .../no_data/blurbs/cloud_deployment.js | 0 .../public/components/no_data/blurbs/index.js | 0 .../components/no_data/blurbs/looking_for.js | 0 .../components/no_data/blurbs/what_is.js | 0 .../components/no_data/checker_errors.js | 0 .../components/no_data/checking_settings.js | 0 .../collection_enabled.test.js.snap | 0 .../__tests__/collection_enabled.test.js | 2 +- .../collection_enabled/collection_enabled.js | 0 .../collection_interval.test.js.snap | 0 .../__tests__/collection_interval.test.js | 2 +- .../collection_interval.js | 0 .../__snapshots__/exporters.test.js.snap | 0 .../exporters/__tests__/exporters.test.js | 2 +- .../explanations/exporters/exporters.js | 0 .../components/no_data/explanations/index.js | 0 .../__snapshots__/plugin_enabled.test.js.snap | 0 .../__tests__/plugin_enabled.test.js | 2 +- .../plugin_enabled/plugin_enabled.js | 0 .../public/components/no_data/index.js | 0 .../public/components/no_data/no_data.js | 0 .../__snapshots__/reason_found.test.js.snap | 0 .../__snapshots__/we_tried.test.js.snap | 0 .../reasons/__tests__/reason_found.test.js | 2 +- .../reasons/__tests__/we_tried.test.js | 2 +- .../components/no_data/reasons/index.js | 0 .../no_data/reasons/reason_found.js | 0 .../components/no_data/reasons/we_tried.js | 0 .../__snapshots__/page_loading.test.js.snap | 0 .../__tests__/page_loading.test.js | 2 +- .../public/components/page_loading/index.js | 0 .../__snapshots__/setup_mode.test.js.snap | 0 .../public/components/renderers/index.js | 0 .../components/renderers/lib/find_new_uuid.js | 0 .../public/components/renderers/setup_mode.js | 0 .../components/renderers/setup_mode.test.js | 0 .../__snapshots__/badge.test.js.snap | 0 .../__snapshots__/enter_button.test.tsx.snap | 0 .../__snapshots__/formatting.test.js.snap | 0 .../listing_callout.test.js.snap | 0 .../__snapshots__/tooltip.test.js.snap | 0 .../components/setup_mode/_enter_button.scss | 0 .../public/components/setup_mode/_index.scss | 0 .../public/components/setup_mode/badge.js | 0 .../components/setup_mode/badge.test.js | 0 .../setup_mode/enter_button.test.tsx | 0 .../components/setup_mode/enter_button.tsx | 0 .../components/setup_mode/formatting.js | 0 .../components/setup_mode/formatting.test.js | 0 .../components/setup_mode/listing_callout.js | 0 .../setup_mode/listing_callout.test.js | 0 .../public/components/setup_mode/tooltip.js | 0 .../components/setup_mode/tooltip.test.js | 0 .../plugins/xpack_main/jquery_flot.js | 0 .../__test__/__snapshots__/index.test.js.snap | 0 .../sparkline/__test__/index.test.js | 4 + .../public/components/sparkline/_index.scss | 0 .../components/sparkline/_sparkline.scss | 0 .../public/components/sparkline/index.js | 0 .../sparkline/sparkline_flot_chart.js | 2 +- .../public/components/status_icon/_index.scss | 0 .../components/status_icon/_status_icon.scss | 0 .../public/components/status_icon/index.js | 0 .../__snapshots__/summary_status.test.js.snap | 0 .../components/summary_status/_index.scss | 0 .../summary_status/_summary_status.scss | 0 .../public/components/summary_status/index.js | 0 .../summary_status/summary_status.js | 0 .../summary_status/summary_status.test.js | 2 +- .../public/components/table/_index.scss | 0 .../public/components/table/_table.scss | 0 .../public/components/table/eui_table.js | 0 .../public/components/table/eui_table_ssp.js | 0 .../public/components/table/index.js | 0 .../public/components/table/storage.js | 0 .../public/directives/beats/beat}/index.js | 19 +- .../directives/beats/overview}/index.js | 18 +- .../elasticsearch/ml_job_listing/index.js | 94 +- .../__tests__/monitoring_main_controller.js | 0 .../public/directives/main/index.html | 0 .../public/directives/main/index.js | 25 +- .../monitoring/public/icons/health-gray.svg | 0 .../monitoring/public/icons/health-green.svg | 0 .../monitoring/public/icons/health-red.svg | 0 .../monitoring/public/icons/health-yellow.svg | 0 .../plugins/monitoring/public/index.scss | 7 + .../monitoring/public}/index.ts | 2 +- .../plugins/monitoring/public/jest.helpers.ts | 9 - .../plugins/monitoring/public/legacy_shims.ts | 83 + .../public/lib/__tests__/format_number.js | 0 .../public/lib/ajax_error_handler.tsx | 11 +- .../public/lib/calculate_shard_stats.js | 0 .../__tests__/enabler.test.js | 0 .../__tests__/settings_checker.test.js | 0 .../__tests__/start_checks.test.js | 0 .../checkers/cluster_settings.js | 0 .../checkers/node_settings.js | 0 .../checkers/settings_checker.js | 0 .../lib/elasticsearch_settings/enabler.js | 0 .../lib/elasticsearch_settings/index.js | 0 .../elasticsearch_settings/start_checks.js | 0 .../public/lib/ensure_minimum_time.js | 0 .../public/lib/ensure_minimum_time.test.js | 0 .../monitoring/public/lib/extract_ip.js | 0 .../monitoring/public/lib/form_validation.ts | 0 .../monitoring/public/lib/format_number.js | 0 .../public/lib/get_cluster_from_clusters.js | 0 .../monitoring/public/lib/get_page_data.js | 4 +- .../public/lib/get_safe_for_external_link.ts | 0 .../public/lib/jquery_flot/flot-charts/API.md | 1498 ++++++++ .../lib/jquery_flot/flot-charts/index.js | 48 + .../flot-charts/jquery.colorhelpers.js | 180 + .../flot-charts/jquery.flot.canvas.js | 345 ++ .../flot-charts/jquery.flot.categories.js | 190 + .../flot-charts/jquery.flot.crosshair.js | 176 + .../flot-charts/jquery.flot.errorbars.js | 353 ++ .../flot-charts/jquery.flot.fillbetween.js | 226 ++ .../flot-charts/jquery.flot.image.js | 241 ++ .../jquery_flot/flot-charts/jquery.flot.js | 3168 +++++++++++++++++ .../flot-charts/jquery.flot.log.js | 163 + .../flot-charts/jquery.flot.navigate.js | 346 ++ .../flot-charts/jquery.flot.pie.js | 824 +++++ .../flot-charts/jquery.flot.resize.js | 59 + .../flot-charts/jquery.flot.selection.js | 360 ++ .../flot-charts/jquery.flot.stack.js | 188 + .../flot-charts/jquery.flot.symbol.js | 71 + .../flot-charts/jquery.flot.threshold.js | 142 + .../flot-charts/jquery.flot.time.js | 473 +++ .../public/lib/jquery_flot}/index.js | 2 +- .../public/lib/jquery_flot/jquery_flot.js | 19 + .../lib/logstash/__tests__/pipelines.js | 0 .../public/lib/logstash/pipelines.js | 0 .../monitoring/public/lib/route_init.js | 2 +- .../monitoring/public/lib/setup_mode.test.js | 112 +- .../monitoring/public/lib/setup_mode.tsx | 39 +- x-pack/plugins/monitoring/public/plugin.ts | 147 + .../public/services/__tests__/breadcrumbs.js | 4 +- .../public/services/__tests__/executor.js} | 12 +- .../public/services/breadcrumbs.js} | 4 +- .../monitoring/public/services/clusters.js | 12 +- .../monitoring/public/services/executor.js} | 55 +- .../monitoring/public/services/features.js | 6 +- .../monitoring/public/services/license.js | 6 +- .../monitoring/public/services/title.js | 23 +- x-pack/plugins/monitoring/public/types.ts | 21 + x-pack/plugins/monitoring/public/url_state.ts | 169 + .../public/views/__tests__/base_controller.js | 7 +- .../views/__tests__/base_table_controller.js | 0 .../public/views/access_denied/index.html | 0 .../public/views/access_denied/index.js | 6 +- .../monitoring/public/views/alerts/index.html | 0 .../monitoring/public/views/alerts/index.js | 41 +- .../plugins/monitoring/public/views/all.js | 1 - .../public/views/apm/instance/index.html | 0 .../public/views/apm/instance/index.js | 19 +- .../public/views/apm/instances/index.html | 0 .../public/views/apm/instances/index.js | 47 +- .../public/views/apm/overview/index.html | 0 .../public/views/apm/overview/index.js | 11 +- .../public/views/base_controller.js | 100 +- .../public/views/base_eui_table_controller.js | 2 +- .../public/views/base_table_controller.js | 2 +- .../public/views/beats/beat/get_page_data.js | 6 +- .../public/views/beats/beat/index.html | 0 .../public/views/beats/beat/index.js | 4 +- .../views/beats/listing/get_page_data.js | 6 +- .../public/views/beats/listing/index.html | 0 .../public/views/beats/listing/index.js | 53 +- .../views/beats/overview/get_page_data.js | 6 +- .../public/views/beats/overview/index.html | 0 .../public/views/beats/overview/index.js | 4 +- .../public/views/cluster/listing/index.html | 0 .../public/views/cluster/listing/index.js | 35 +- .../public/views/cluster/overview/index.html | 0 .../public/views/cluster/overview/index.js | 47 +- .../views/elasticsearch/ccr/get_page_data.js | 6 +- .../public/views/elasticsearch/ccr/index.html | 0 .../public/views/elasticsearch/ccr/index.js | 11 +- .../elasticsearch/ccr/shard/get_page_data.js | 6 +- .../views/elasticsearch/ccr/shard/index.html | 0 .../views/elasticsearch/ccr/shard/index.js | 11 +- .../elasticsearch/index/advanced/index.html | 0 .../elasticsearch/index/advanced/index.js | 25 +- .../views/elasticsearch/index/index.html | 0 .../public/views/elasticsearch/index/index.js | 31 +- .../views/elasticsearch/indices/index.html | 0 .../views/elasticsearch/indices/index.js | 25 +- .../elasticsearch/ml_jobs/get_page_data.js | 6 +- .../views/elasticsearch/ml_jobs/index.html | 0 .../views/elasticsearch/ml_jobs/index.js | 4 +- .../elasticsearch/node/advanced/index.html | 0 .../elasticsearch/node/advanced/index.js | 25 +- .../views/elasticsearch/node/get_page_data.js | 6 +- .../views/elasticsearch/node/index.html | 0 .../public/views/elasticsearch/node/index.js | 25 +- .../views/elasticsearch/nodes/index.html | 0 .../public/views/elasticsearch/nodes/index.js | 49 +- .../elasticsearch/overview/controller.js | 27 +- .../views/elasticsearch/overview/index.html | 0 .../views/elasticsearch/overview/index.js | 4 +- .../plugins/monitoring/public/views/index.js | 0 .../public/views/kibana/instance/index.html | 0 .../public/views/kibana/instance/index.js | 150 + .../views/kibana/instances/get_page_data.js | 6 +- .../public/views/kibana/instances/index.html | 0 .../public/views/kibana/instances/index.js | 55 +- .../public/views/kibana/overview/index.html | 0 .../public/views/kibana/overview/index.js | 65 +- .../public/views/license/controller.js | 34 +- .../public/views/license/index.html | 0 .../monitoring/public/views/license/index.js | 4 +- .../views/logstash/node/advanced/index.html | 0 .../views/logstash/node/advanced/index.js | 61 +- .../public/views/logstash/node/index.html | 0 .../public/views/logstash/node/index.js | 61 +- .../views/logstash/node/pipelines/index.html | 0 .../views/logstash/node/pipelines/index.js | 45 +- .../views/logstash/nodes/get_page_data.js | 6 +- .../public/views/logstash/nodes/index.html | 0 .../public/views/logstash/nodes/index.js | 47 +- .../public/views/logstash/overview/index.html | 0 .../public/views/logstash/overview/index.js | 25 +- .../public/views/logstash/pipeline/index.html | 0 .../public/views/logstash/pipeline/index.js | 47 +- .../views/logstash/pipelines/index.html | 0 .../public/views/logstash/pipelines/index.js | 41 +- .../no_data/__tests__/model_updater.test.js | 0 .../public/views/no_data/controller.js | 25 +- .../public/views/no_data/index.html | 0 .../monitoring/public/views/no_data/index.js | 12 +- .../public/views/no_data/model_updater.js | 0 x-pack/plugins/monitoring/server/index.ts | 6 + .../spaces_usage_collector.ts | 2 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 569 files changed, 11410 insertions(+), 2967 deletions(-) delete mode 100644 x-pack/legacy/plugins/monitoring/.agignore delete mode 100644 x-pack/legacy/plugins/monitoring/common/__tests__/format_timestamp_to_duration.js delete mode 100644 x-pack/legacy/plugins/monitoring/common/cancel_promise.ts delete mode 100644 x-pack/legacy/plugins/monitoring/common/constants.ts delete mode 100644 x-pack/legacy/plugins/monitoring/common/format_timestamp_to_duration.js delete mode 100644 x-pack/legacy/plugins/monitoring/common/formatting.js rename x-pack/legacy/plugins/monitoring/{config.js => config.ts} (99%) delete mode 100644 x-pack/legacy/plugins/monitoring/index.js create mode 100644 x-pack/legacy/plugins/monitoring/index.ts delete mode 100644 x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.test.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/components/license/index.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/directives/all.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/filters/index.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/hacks/__tests__/toggle_app_link_in_nav.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/icons/monitoring.svg delete mode 100644 x-pack/legacy/plugins/monitoring/public/legacy.ts delete mode 100644 x-pack/legacy/plugins/monitoring/public/np_imports/angular/angular_config.ts delete mode 100644 x-pack/legacy/plugins/monitoring/public/np_imports/angular/index.ts delete mode 100644 x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts delete mode 100644 x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/promises.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts delete mode 100644 x-pack/legacy/plugins/monitoring/public/np_imports/ui/capabilities.ts delete mode 100644 x-pack/legacy/plugins/monitoring/public/np_imports/ui/chrome.ts delete mode 100644 x-pack/legacy/plugins/monitoring/public/np_imports/ui/modules.ts delete mode 100644 x-pack/legacy/plugins/monitoring/public/np_imports/ui/timefilter.ts delete mode 100644 x-pack/legacy/plugins/monitoring/public/np_ready/plugin.ts delete mode 100644 x-pack/legacy/plugins/monitoring/public/register_feature.ts delete mode 100644 x-pack/legacy/plugins/monitoring/public/services/breadcrumbs.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/services/executor.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/views/kibana/instance/index.js delete mode 100644 x-pack/legacy/plugins/monitoring/public/views/loading/index.html delete mode 100644 x-pack/legacy/plugins/monitoring/public/views/loading/index.js delete mode 100644 x-pack/legacy/plugins/monitoring/ui_exports.js rename x-pack/{legacy => }/plugins/monitoring/public/_hacks.scss (100%) create mode 100644 x-pack/plugins/monitoring/public/angular/app_modules.ts rename x-pack/{legacy/plugins/monitoring/public/np_imports/ui => plugins/monitoring/public/angular/helpers}/routes.ts (62%) rename x-pack/{legacy/plugins/monitoring/public/np_imports/ui => plugins/monitoring/public/angular/helpers}/utils.ts (100%) create mode 100644 x-pack/plugins/monitoring/public/angular/index.ts rename x-pack/{legacy/plugins/monitoring/public/np_imports => plugins/monitoring/public}/angular/providers/private.js (99%) create mode 100644 x-pack/plugins/monitoring/public/angular/providers/url.js rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/__tests__/map_severity.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/alerts.js (95%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx (91%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/configuration.tsx (96%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/index.ts (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx (83%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/step1.tsx (97%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/step2.tsx (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/configuration/step3.tsx (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/formatted_alert.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/manage_email_action.tsx (99%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/map_severity.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/status.test.tsx (83%) rename x-pack/{legacy => }/plugins/monitoring/public/components/alerts/status.tsx (93%) rename x-pack/{legacy => }/plugins/monitoring/public/components/apm/instance/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/apm/instance/instance.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/apm/instance/status.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/apm/instances/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/apm/instances/instances.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/apm/instances/status.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/apm/overview/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/apm/status_icon.js (91%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/beat/beat.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/beat/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/listing/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/listing/listing.js (96%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_active.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_types.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_versions.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/latest_active.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/latest_active.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/latest_types.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/latest_types.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/latest_versions.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/latest_versions.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/overview.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/overview/overview.test.js (93%) rename x-pack/{legacy => }/plugins/monitoring/public/components/beats/stats.js (95%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/__tests__/get_color.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/__tests__/get_last_value.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/__tests__/get_title.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/__tests__/get_values_for_legend.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/_chart.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/_index.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/chart_target.js (99%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/event_bus.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/get_chart_options.js (92%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/get_color.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/get_last_value.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/get_title.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/get_units.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/get_values_for_legend.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/horizontal_legend.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/info_tooltip.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/monitoring_timeseries.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/timeseries_container.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/chart/timeseries_visualization.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js (97%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/listing/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/listing/listing.js (95%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/overview/__tests__/__snapshots__/helpers.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js (93%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/overview/alerts_panel.js (97%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/overview/apm_panel.js (98%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/overview/beats_panel.js (98%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js (99%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/overview/helpers.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/overview/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/overview/kibana_panel.js (98%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/overview/license_text.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/overview/logstash_panel.js (99%) rename x-pack/{legacy => }/plugins/monitoring/public/components/cluster/status_icon.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/ccr/__snapshots__/ccr.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js (99%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/ccr/index.js (100%) rename x-pack/{legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.css => plugins/monitoring/public/components/elasticsearch/ccr/index.scss} (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js (97%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js (88%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/index/advanced.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/index/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/indices/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/indices/indices.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js (93%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/node/advanced.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/node/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/node/node.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/node/status_icon.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js (95%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/nodes/cells.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/nodes/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/overview/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/overview/overview.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js (92%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js (98%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_activity/source_destination.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/_index.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/__snapshots__/shard.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/calculate_class.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/generate_query_and_link.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_unassigned.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/labels.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/elasticsearch/status_icon.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/kibana/cluster_status/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/kibana/detail_status/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/kibana/instances/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/kibana/instances/instances.js (99%) rename x-pack/{legacy => }/plugins/monitoring/public/components/kibana/status_icon.js (91%) create mode 100644 x-pack/plugins/monitoring/public/components/license/index.js rename x-pack/{legacy => }/plugins/monitoring/public/components/logs/__snapshots__/logs.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logs/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logs/logs.js (95%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logs/logs.test.js (95%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logs/reason.js (98%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logs/reason.test.js (93%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/cluster_status/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/detail_status/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/listing/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/listing/listing.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/listing/listing.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/overview/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/overview/overview.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_listing/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js (98%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/config.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/pipeline_state.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/config.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/boolean_edge.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge_factory.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/if_vertex.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/plugin_vertex.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/queue_vertex.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex_factory.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/boolean_edge.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge_factory.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/if_vertex.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/plugin_vertex.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/queue_vertex.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex_factory.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/element.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/if_statement.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/make_statement.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/pipeline.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/plugin_statement.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/queue.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/statement.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/utils.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/if_statement.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/make_statement.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/pipeline.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/plugin_statement.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/queue.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/statement.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/utils.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline_state.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/collapsible_statement.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/metric.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/pipeline_viewer.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/plugin_statement.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/queue.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list_heading.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_section.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/collapsible_statement.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js (98%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/metric.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js (94%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/queue.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list_heading.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_section.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_index.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_pipeline_viewer.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/collapsible_statement.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/metric.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/queue.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list_heading.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_section.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/constants.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js (98%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js (95%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/flyout/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/disable_internal_collection_instructions.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js (97%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js (98%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/monospace.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js (97%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js (97%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js (97%) rename x-pack/{legacy => }/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/checker_errors.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js (92%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js (84%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/_index.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/_no_data.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/blurbs/changes_needed.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/blurbs/cloud_deployment.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/blurbs/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/blurbs/looking_for.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/blurbs/what_is.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/checker_errors.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/checking_settings.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js (93%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js (96%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/__snapshots__/exporters.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js (93%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/exporters/exporters.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/__snapshots__/plugin_enabled.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js (92%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/plugin_enabled.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/no_data.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/we_tried.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js (96%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js (85%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/reasons/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/reasons/reason_found.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/no_data/reasons/we_tried.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js (85%) rename x-pack/{legacy => }/plugins/monitoring/public/components/page_loading/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/renderers/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/renderers/lib/find_new_uuid.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/renderers/setup_mode.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/renderers/setup_mode.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/__snapshots__/badge.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/__snapshots__/formatting.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/__snapshots__/listing_callout.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/_enter_button.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/_index.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/badge.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/badge.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/enter_button.tsx (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/formatting.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/formatting.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/listing_callout.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/listing_callout.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/tooltip.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/setup_mode/tooltip.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/sparkline/__test__/__snapshots__/index.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/sparkline/__test__/index.test.js (95%) rename x-pack/{legacy => }/plugins/monitoring/public/components/sparkline/_index.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/sparkline/_sparkline.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/sparkline/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js (98%) rename x-pack/{legacy => }/plugins/monitoring/public/components/status_icon/_index.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/status_icon/_status_icon.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/status_icon/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/summary_status/_index.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/summary_status/_summary_status.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/summary_status/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/summary_status/summary_status.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/summary_status/summary_status.test.js (95%) rename x-pack/{legacy => }/plugins/monitoring/public/components/table/_index.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/table/_table.scss (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/table/eui_table.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/table/eui_table_ssp.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/table/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/components/table/storage.js (100%) rename x-pack/{legacy/plugins/monitoring/public/directives/beats/overview => plugins/monitoring/public/directives/beats/beat}/index.js (59%) rename x-pack/{legacy/plugins/monitoring/public/directives/beats/beat => plugins/monitoring/public/directives/beats/overview}/index.js (55%) rename x-pack/{legacy => }/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js (63%) rename x-pack/{legacy => }/plugins/monitoring/public/directives/main/__tests__/monitoring_main_controller.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/directives/main/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/directives/main/index.js (93%) rename x-pack/{legacy => }/plugins/monitoring/public/icons/health-gray.svg (100%) rename x-pack/{legacy => }/plugins/monitoring/public/icons/health-green.svg (100%) rename x-pack/{legacy => }/plugins/monitoring/public/icons/health-red.svg (100%) rename x-pack/{legacy => }/plugins/monitoring/public/icons/health-yellow.svg (100%) rename x-pack/{legacy => }/plugins/monitoring/public/index.scss (83%) rename x-pack/{legacy/plugins/monitoring/public/np_ready => plugins/monitoring/public}/index.ts (84%) rename x-pack/{legacy => }/plugins/monitoring/public/jest.helpers.ts (80%) create mode 100644 x-pack/plugins/monitoring/public/legacy_shims.ts rename x-pack/{legacy => }/plugins/monitoring/public/lib/__tests__/format_number.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/ajax_error_handler.tsx (88%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/calculate_shard_stats.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/enabler.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/settings_checker.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/start_checks.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/elasticsearch_settings/checkers/cluster_settings.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/elasticsearch_settings/checkers/node_settings.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/elasticsearch_settings/checkers/settings_checker.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/elasticsearch_settings/enabler.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/elasticsearch_settings/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/elasticsearch_settings/start_checks.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/ensure_minimum_time.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/ensure_minimum_time.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/extract_ip.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/form_validation.ts (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/format_number.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/get_cluster_from_clusters.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/get_page_data.js (87%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/get_safe_for_external_link.ts (100%) create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/API.md create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.colorhelpers.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.canvas.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.categories.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.crosshair.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.errorbars.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.fillbetween.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.image.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.log.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.navigate.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.pie.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.resize.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.selection.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.stack.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.symbol.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.threshold.js create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js rename x-pack/{legacy/plugins/monitoring/common => plugins/monitoring/public/lib/jquery_flot}/index.js (76%) create mode 100644 x-pack/plugins/monitoring/public/lib/jquery_flot/jquery_flot.js rename x-pack/{legacy => }/plugins/monitoring/public/lib/logstash/__tests__/pipelines.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/logstash/pipelines.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/route_init.js (95%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/setup_mode.test.js (72%) rename x-pack/{legacy => }/plugins/monitoring/public/lib/setup_mode.tsx (82%) create mode 100644 x-pack/plugins/monitoring/public/plugin.ts rename x-pack/{legacy => }/plugins/monitoring/public/services/__tests__/breadcrumbs.js (96%) rename x-pack/{legacy/plugins/monitoring/public/services/__tests__/executor_provider.js => plugins/monitoring/public/services/__tests__/executor.js} (87%) rename x-pack/{legacy/plugins/monitoring/public/services/breadcrumbs_provider.js => plugins/monitoring/public/services/breadcrumbs.js} (98%) rename x-pack/{legacy => }/plugins/monitoring/public/services/clusters.js (77%) rename x-pack/{legacy/plugins/monitoring/public/services/executor_provider.js => plugins/monitoring/public/services/executor.js} (66%) rename x-pack/{legacy => }/plugins/monitoring/public/services/features.js (86%) rename x-pack/{legacy => }/plugins/monitoring/public/services/license.js (88%) rename x-pack/{legacy => }/plugins/monitoring/public/services/title.js (54%) create mode 100644 x-pack/plugins/monitoring/public/types.ts create mode 100644 x-pack/plugins/monitoring/public/url_state.ts rename x-pack/{legacy => }/plugins/monitoring/public/views/__tests__/base_controller.js (95%) rename x-pack/{legacy => }/plugins/monitoring/public/views/__tests__/base_table_controller.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/access_denied/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/access_denied/index.js (89%) rename x-pack/{legacy => }/plugins/monitoring/public/views/alerts/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/alerts/index.js (77%) rename x-pack/{legacy => }/plugins/monitoring/public/views/all.js (98%) rename x-pack/{legacy => }/plugins/monitoring/public/views/apm/instance/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/apm/instance/index.js (83%) rename x-pack/{legacy => }/plugins/monitoring/public/views/apm/instances/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/apm/instances/index.js (70%) rename x-pack/{legacy => }/plugins/monitoring/public/views/apm/overview/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/apm/overview/index.js (81%) rename x-pack/{legacy => }/plugins/monitoring/public/views/base_controller.js (77%) rename x-pack/{legacy => }/plugins/monitoring/public/views/base_eui_table_controller.js (97%) rename x-pack/{legacy => }/plugins/monitoring/public/views/base_table_controller.js (95%) rename x-pack/{legacy => }/plugins/monitoring/public/views/beats/beat/get_page_data.js (82%) rename x-pack/{legacy => }/plugins/monitoring/public/views/beats/beat/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/beats/beat/index.js (92%) rename x-pack/{legacy => }/plugins/monitoring/public/views/beats/listing/get_page_data.js (80%) rename x-pack/{legacy => }/plugins/monitoring/public/views/beats/listing/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/beats/listing/index.js (66%) rename x-pack/{legacy => }/plugins/monitoring/public/views/beats/overview/get_page_data.js (80%) rename x-pack/{legacy => }/plugins/monitoring/public/views/beats/overview/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/beats/overview/index.js (91%) rename x-pack/{legacy => }/plugins/monitoring/public/views/cluster/listing/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/cluster/listing/index.js (75%) rename x-pack/{legacy => }/plugins/monitoring/public/views/cluster/overview/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/cluster/overview/index.js (68%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js (80%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/ccr/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/ccr/index.js (83%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js (82%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js (86%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/index/advanced/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js (79%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/index/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/index/index.js (80%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/indices/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/indices/index.js (80%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js (80%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js (92%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/node/advanced/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js (78%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js (84%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/node/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/node/index.js (84%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/nodes/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/nodes/index.js (74%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/overview/controller.js (83%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/overview/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/elasticsearch/overview/index.js (84%) rename x-pack/{legacy => }/plugins/monitoring/public/views/index.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/kibana/instance/index.html (100%) create mode 100644 x-pack/plugins/monitoring/public/views/kibana/instance/index.js rename x-pack/{legacy => }/plugins/monitoring/public/views/kibana/instances/get_page_data.js (80%) rename x-pack/{legacy => }/plugins/monitoring/public/views/kibana/instances/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/kibana/instances/index.js (56%) rename x-pack/{legacy => }/plugins/monitoring/public/views/kibana/overview/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/kibana/overview/index.js (58%) rename x-pack/{legacy => }/plugins/monitoring/public/views/license/controller.js (71%) rename x-pack/{legacy => }/plugins/monitoring/public/views/license/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/license/index.js (83%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/node/advanced/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/node/advanced/index.js (65%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/node/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/node/index.js (65%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/node/pipelines/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/node/pipelines/index.js (74%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/nodes/get_page_data.js (80%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/nodes/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/nodes/index.js (57%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/overview/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/overview/index.js (73%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/pipeline/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/pipeline/index.js (77%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/pipelines/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/logstash/pipelines/index.js (75%) rename x-pack/{legacy => }/plugins/monitoring/public/views/no_data/__tests__/model_updater.test.js (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/no_data/controller.js (87%) rename x-pack/{legacy => }/plugins/monitoring/public/views/no_data/index.html (100%) rename x-pack/{legacy => }/plugins/monitoring/public/views/no_data/index.js (63%) rename x-pack/{legacy => }/plugins/monitoring/public/views/no_data/model_updater.js (100%) diff --git a/.eslintignore b/.eslintignore index 2ed9ecf971ff3..4913192e81c1d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -30,6 +30,7 @@ target /x-pack/legacy/plugins/canvas/canvas_plugin_src/lib/flot-charts /x-pack/legacy/plugins/canvas/shareable_runtime/build /x-pack/legacy/plugins/canvas/storybook +/x-pack/plugins/monitoring/public/lib/jquery_flot /x-pack/legacy/plugins/infra/common/graphql/types.ts /x-pack/legacy/plugins/infra/public/graphql/types.ts /x-pack/legacy/plugins/infra/server/graphql/types.ts diff --git a/.eslintrc.js b/.eslintrc.js index c9b41ec711b7f..8b33ec83347a8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -963,6 +963,12 @@ module.exports = { jquery: true, }, }, + { + files: ['x-pack/plugins/monitoring/public/lib/jquery_flot/**/*.js'], + env: { + jquery: true, + }, + }, /** * TSVB overrides diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c17538849660e..3981a8e1e9afe 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -84,6 +84,7 @@ /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management /x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest-management /x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui +/x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/uptime @elastic/uptime # Machine Learning diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 66296736b3ad0..14e25ab863dbc 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -38,6 +38,7 @@ export const IGNORE_FILE_GLOBS = [ 'x-pack/legacy/plugins/apm/**/*', 'x-pack/legacy/plugins/canvas/tasks/**/*', 'x-pack/legacy/plugins/canvas/canvas_plugin_src/**/*', + 'x-pack/plugins/monitoring/public/lib/jquery_flot/**/*', '**/.*', '**/{webpackShims,__mocks__}/**/*', 'x-pack/docs/**/*', @@ -160,12 +161,11 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'webpackShims/ui-bootstrap.js', 'x-pack/legacy/plugins/index_management/public/lib/editSettings.js', 'x-pack/legacy/plugins/license_management/public/store/reducers/licenseManagement.js', - 'x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js', - 'x-pack/legacy/plugins/monitoring/public/icons/alert-blue.svg', - 'x-pack/legacy/plugins/monitoring/public/icons/health-gray.svg', - 'x-pack/legacy/plugins/monitoring/public/icons/health-green.svg', - 'x-pack/legacy/plugins/monitoring/public/icons/health-red.svg', - 'x-pack/legacy/plugins/monitoring/public/icons/health-yellow.svg', + 'x-pack/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js', + 'x-pack/plugins/monitoring/public/icons/health-gray.svg', + 'x-pack/plugins/monitoring/public/icons/health-green.svg', + 'x-pack/plugins/monitoring/public/icons/health-red.svg', + 'x-pack/plugins/monitoring/public/icons/health-yellow.svg', 'x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', 'x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', 'x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf', diff --git a/x-pack/legacy/plugins/monitoring/.agignore b/x-pack/legacy/plugins/monitoring/.agignore deleted file mode 100644 index 10fb4038cbdc2..0000000000000 --- a/x-pack/legacy/plugins/monitoring/.agignore +++ /dev/null @@ -1,3 +0,0 @@ -agent -node_modules -bower_components diff --git a/x-pack/legacy/plugins/monitoring/common/__tests__/format_timestamp_to_duration.js b/x-pack/legacy/plugins/monitoring/common/__tests__/format_timestamp_to_duration.js deleted file mode 100644 index 470d596bd2bdc..0000000000000 --- a/x-pack/legacy/plugins/monitoring/common/__tests__/format_timestamp_to_duration.js +++ /dev/null @@ -1,128 +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 expect from '@kbn/expect'; -import moment from 'moment'; -import { formatTimestampToDuration } from '../format_timestamp_to_duration'; -import { CALCULATE_DURATION_SINCE, CALCULATE_DURATION_UNTIL } from '../constants'; - -const testTime = moment('2010-05-01'); // pick a date where adding/subtracting 2 months formats roundly to '2 months 0 days' -const getTestTime = () => moment(testTime); // clones the obj so it's not mutated with .adds and .subtracts - -/** - * Test the moment-duration-format template - */ -describe('formatTimestampToDuration', () => { - describe('format timestamp to duration - time since', () => { - it('should format timestamp to human-readable duration', () => { - // time inputs are a few "moments" extra from the time advertised by name - const fiftyNineSeconds = getTestTime().subtract(59, 'seconds'); - expect( - formatTimestampToDuration(fiftyNineSeconds, CALCULATE_DURATION_SINCE, getTestTime()) - ).to.be('59 seconds'); - - const fiveMins = getTestTime() - .subtract(5, 'minutes') - .subtract(30, 'seconds'); - expect(formatTimestampToDuration(fiveMins, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '6 mins' - ); - - const sixHours = getTestTime() - .subtract(6, 'hours') - .subtract(30, 'minutes'); - expect(formatTimestampToDuration(sixHours, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '6 hrs 30 mins' - ); - - const sevenDays = getTestTime() - .subtract(7, 'days') - .subtract(6, 'hours') - .subtract(18, 'minutes'); - expect(formatTimestampToDuration(sevenDays, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '7 days 6 hrs 18 mins' - ); - - const eightWeeks = getTestTime() - .subtract(8, 'weeks') - .subtract(7, 'days') - .subtract(6, 'hours') - .subtract(18, 'minutes'); - expect(formatTimestampToDuration(eightWeeks, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '2 months 2 days' - ); - - const oneHour = getTestTime().subtract(1, 'hour'); // should trim 0 min - expect(formatTimestampToDuration(oneHour, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '1 hr' - ); - - const oneDay = getTestTime().subtract(1, 'day'); // should trim 0 hrs - expect(formatTimestampToDuration(oneDay, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '1 day' - ); - - const twoMonths = getTestTime().subtract(2, 'month'); // should trim 0 days - expect(formatTimestampToDuration(twoMonths, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '2 months' - ); - }); - }); - - describe('format timestamp to duration - time until', () => { - it('should format timestamp to human-readable duration', () => { - // time inputs are a few "moments" extra from the time advertised by name - const fiftyNineSeconds = getTestTime().add(59, 'seconds'); - expect( - formatTimestampToDuration(fiftyNineSeconds, CALCULATE_DURATION_UNTIL, getTestTime()) - ).to.be('59 seconds'); - - const fiveMins = getTestTime().add(10, 'minutes'); - expect(formatTimestampToDuration(fiveMins, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '10 mins' - ); - - const sixHours = getTestTime() - .add(6, 'hours') - .add(30, 'minutes'); - expect(formatTimestampToDuration(sixHours, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '6 hrs 30 mins' - ); - - const sevenDays = getTestTime() - .add(7, 'days') - .add(6, 'hours') - .add(18, 'minutes'); - expect(formatTimestampToDuration(sevenDays, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '7 days 6 hrs 18 mins' - ); - - const eightWeeks = getTestTime() - .add(8, 'weeks') - .add(7, 'days') - .add(6, 'hours') - .add(18, 'minutes'); - expect(formatTimestampToDuration(eightWeeks, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '2 months 2 days' - ); - - const oneHour = getTestTime().add(1, 'hour'); // should trim 0 min - expect(formatTimestampToDuration(oneHour, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '1 hr' - ); - - const oneDay = getTestTime().add(1, 'day'); // should trim 0 hrs - expect(formatTimestampToDuration(oneDay, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '1 day' - ); - - const twoMonths = getTestTime().add(2, 'month'); // should trim 0 days - expect(formatTimestampToDuration(twoMonths, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '2 months' - ); - }); - }); -}); diff --git a/x-pack/legacy/plugins/monitoring/common/cancel_promise.ts b/x-pack/legacy/plugins/monitoring/common/cancel_promise.ts deleted file mode 100644 index f100edda50796..0000000000000 --- a/x-pack/legacy/plugins/monitoring/common/cancel_promise.ts +++ /dev/null @@ -1,70 +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. - */ - -export enum Status { - Canceled, - Failed, - Resolved, - Awaiting, - Idle, -} - -/** - * Simple [PromiseWithCancel] factory - */ -export class PromiseWithCancel { - private _promise: Promise; - private _status: Status = Status.Idle; - - /** - * @param {Promise} promise Promise you want to cancel / track - */ - constructor(promise: Promise) { - this._promise = promise; - } - - /** - * Cancel the promise in any state - */ - public cancel = (): void => { - this._status = Status.Canceled; - }; - - /** - * @returns status based on [Status] - */ - public status = (): Status => { - return this._status; - }; - - /** - * @returns promise passed in [constructor] - * This sets the state to Status.Awaiting - */ - public promise = (): Promise => { - if (this._status === Status.Canceled) { - throw Error('Getting a canceled promise is not allowed'); - } else if (this._status !== Status.Idle) { - return this._promise; - } - return new Promise((resolve, reject) => { - this._status = Status.Awaiting; - return this._promise - .then(response => { - if (this._status !== Status.Canceled) { - this._status = Status.Resolved; - return resolve(response); - } - }) - .catch(error => { - if (this._status !== Status.Canceled) { - this._status = Status.Failed; - return reject(error); - } - }); - }); - }; -} diff --git a/x-pack/legacy/plugins/monitoring/common/constants.ts b/x-pack/legacy/plugins/monitoring/common/constants.ts deleted file mode 100644 index 36030e1fa7f2a..0000000000000 --- a/x-pack/legacy/plugins/monitoring/common/constants.ts +++ /dev/null @@ -1,268 +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. - */ - -/** - * Helper string to add as a tag in every logging call - */ -export const LOGGING_TAG = 'monitoring'; -/** - * Helper string to add as a tag in every logging call related to Kibana monitoring - */ -export const KIBANA_MONITORING_LOGGING_TAG = 'kibana-monitoring'; - -/** - * The Monitoring API version is the expected API format that we export and expect to import. - * @type {string} - */ -export const MONITORING_SYSTEM_API_VERSION = '7'; -/** - * The type name used within the Monitoring index to publish Kibana ops stats. - * @type {string} - */ -export const KIBANA_STATS_TYPE_MONITORING = 'kibana_stats'; // similar to KIBANA_STATS_TYPE but rolled up into 10s stats from 5s intervals through ops_buffer -/** - * The type name used within the Monitoring index to publish Kibana stats. - * @type {string} - */ -export const KIBANA_SETTINGS_TYPE = 'kibana_settings'; -/** - * The type name used within the Monitoring index to publish Kibana usage stats. - * NOTE: this string shows as-is in the stats API as a field name for the kibana usage stats - * @type {string} - */ -export const KIBANA_USAGE_TYPE = 'kibana'; - -/* - * Key for the localStorage service - */ -export const STORAGE_KEY = 'xpack.monitoring.data'; - -/** - * Units for derivative metric values - */ -export const NORMALIZED_DERIVATIVE_UNIT = '1s'; - -/* - * Values for column sorting in table options - * @type {number} 1 or -1 - */ -export const EUI_SORT_ASCENDING = 'asc'; -export const EUI_SORT_DESCENDING = 'desc'; -export const SORT_ASCENDING = 1; -export const SORT_DESCENDING = -1; - -/* - * Chart colors - * @type {string} - */ -export const CHART_LINE_COLOR = '#d2d2d2'; -export const CHART_TEXT_COLOR = '#9c9c9c'; - -/* - * Number of cluster alerts to show on overview page - * @type {number} - */ -export const CLUSTER_ALERTS_SEARCH_SIZE = 3; - -/* - * Format for moment-duration-format timestamp-to-duration template if the time diffs are gte 1 month - * @type {string} - */ -export const FORMAT_DURATION_TEMPLATE_LONG = 'M [months] d [days]'; - -/* - * Format for moment-duration-format timestamp-to-duration template if the time diffs are lt 1 month but gt 1 minute - * @type {string} - */ -export const FORMAT_DURATION_TEMPLATE_SHORT = ' d [days] h [hrs] m [min]'; - -/* - * Format for moment-duration-format timestamp-to-duration template if the time diffs are lt 1 minute - * @type {string} - */ -export const FORMAT_DURATION_TEMPLATE_TINY = ' s [seconds]'; - -/* - * Simple unique values for Timestamp to duration flags. These are used for - * determining if calculation should be formatted as "time until" (now to - * timestamp) or "time since" (timestamp to now) - */ -export const CALCULATE_DURATION_SINCE = 'since'; -export const CALCULATE_DURATION_UNTIL = 'until'; - -/** - * In order to show ML Jobs tab in the Elasticsearch section / tab navigation, license must be supported - */ -export const ML_SUPPORTED_LICENSES = ['trial', 'platinum', 'enterprise']; - -/** - * Metadata service URLs for the different cloud services that have constant URLs (e.g., unlike GCP, which is a constant prefix). - * - * @type {Object} - */ -export const CLOUD_METADATA_SERVICES = { - // We explicitly call out the version, 2016-09-02, rather than 'latest' to avoid unexpected changes - AWS_URL: 'http://169.254.169.254/2016-09-02/dynamic/instance-identity/document', - - // 2017-04-02 is the first GA release of this API - AZURE_URL: 'http://169.254.169.254/metadata/instance?api-version=2017-04-02', - - // GCP documentation shows both 'metadata.google.internal' (mostly) and '169.254.169.254' (sometimes) - // To bypass potential DNS changes, the IP was used because it's shared with other cloud services - GCP_URL_PREFIX: 'http://169.254.169.254/computeMetadata/v1/instance', -}; - -/** - * Constants used by Logstash monitoring code - */ -export const LOGSTASH = { - MAJOR_VER_REQD_FOR_PIPELINES: 6, - - /* - * Names ES keys on for different Logstash pipeline queues. - * @type {string} - */ - QUEUE_TYPES: { - MEMORY: 'memory', - PERSISTED: 'persisted', - }, -}; - -export const DEBOUNCE_SLOW_MS = 17; // roughly how long it takes to render a frame at 60fps -export const DEBOUNCE_FAST_MS = 10; // roughly how long it takes to render a frame at 100fps - -/** - * Configuration key for setting the email address used for cluster alert notifications. - */ -export const CLUSTER_ALERTS_ADDRESS_CONFIG_KEY = 'cluster_alerts.email_notifications.email_address'; - -export const STANDALONE_CLUSTER_CLUSTER_UUID = '__standalone_cluster__'; - -export const INDEX_PATTERN = '.monitoring-*-6-*,.monitoring-*-7-*'; -export const INDEX_PATTERN_KIBANA = '.monitoring-kibana-6-*,.monitoring-kibana-7-*'; -export const INDEX_PATTERN_LOGSTASH = '.monitoring-logstash-6-*,.monitoring-logstash-7-*'; -export const INDEX_PATTERN_BEATS = '.monitoring-beats-6-*,.monitoring-beats-7-*'; -export const INDEX_ALERTS = '.monitoring-alerts-6,.monitoring-alerts-7'; -export const INDEX_PATTERN_ELASTICSEARCH = '.monitoring-es-6-*,.monitoring-es-7-*'; - -// This is the unique token that exists in monitoring indices collected by metricbeat -export const METRICBEAT_INDEX_NAME_UNIQUE_TOKEN = '-mb-'; - -// We use this for metricbeat migration to identify specific products that we do not have constants for -export const ELASTICSEARCH_SYSTEM_ID = 'elasticsearch'; - -/** - * The id of the infra source owned by the monitoring plugin. - */ -export const INFRA_SOURCE_ID = 'internal-stack-monitoring'; - -/* - * These constants represent code paths within `getClustersFromRequest` - * that an api call wants to invoke. This is meant as an optimization to - * avoid unnecessary ES queries (looking at you logstash) when the data - * is not used. In the long term, it'd be nice to have separate api calls - * instead of this path logic. - */ -export const CODE_PATH_ALL = 'all'; -export const CODE_PATH_ALERTS = 'alerts'; -export const CODE_PATH_KIBANA = 'kibana'; -export const CODE_PATH_ELASTICSEARCH = 'elasticsearch'; -export const CODE_PATH_ML = 'ml'; -export const CODE_PATH_BEATS = 'beats'; -export const CODE_PATH_LOGSTASH = 'logstash'; -export const CODE_PATH_APM = 'apm'; -export const CODE_PATH_LICENSE = 'license'; -export const CODE_PATH_LOGS = 'logs'; - -/** - * The header sent by telemetry service when hitting Elasticsearch to identify query source - * @type {string} - */ -export const TELEMETRY_QUERY_SOURCE = 'TELEMETRY'; - -/** - * The name of the Kibana System ID used to publish and look up Kibana stats through the Monitoring system. - * @type {string} - */ -export const KIBANA_SYSTEM_ID = 'kibana'; - -/** - * The name of the Beats System ID used to publish and look up Beats stats through the Monitoring system. - * @type {string} - */ -export const BEATS_SYSTEM_ID = 'beats'; - -/** - * The name of the Apm System ID used to publish and look up Apm stats through the Monitoring system. - * @type {string} - */ -export const APM_SYSTEM_ID = 'apm'; - -/** - * The name of the Kibana System ID used to look up Logstash stats through the Monitoring system. - * @type {string} - */ -export const LOGSTASH_SYSTEM_ID = 'logstash'; - -/** - * The name of the Kibana System ID used to look up Reporting stats through the Monitoring system. - * @type {string} - */ -export const REPORTING_SYSTEM_ID = 'reporting'; - -/** - * The amount of time, in milliseconds, to wait between collecting kibana stats from es. - * - * Currently 24 hours kept in sync with reporting interval. - * @type {Number} - */ -export const TELEMETRY_COLLECTION_INTERVAL = 86400000; - -/** - * We want to slowly rollout the migration from watcher-based cluster alerts to - * kibana alerts and we only want to enable the kibana alerts once all - * watcher-based cluster alerts have been migrated so this flag will serve - * as the only way to see the new UI and actually run Kibana alerts. It will - * be false until all alerts have been migrated, then it will be removed - */ -export const KIBANA_ALERTING_ENABLED = false; - -/** - * The prefix for all alert types used by monitoring - */ -export const ALERT_TYPE_PREFIX = 'monitoring_'; - -/** - * This is the alert type id for the license expiration alert - */ -export const ALERT_TYPE_LICENSE_EXPIRATION = `${ALERT_TYPE_PREFIX}alert_type_license_expiration`; -/** - * This is the alert type id for the cluster state alert - */ -export const ALERT_TYPE_CLUSTER_STATE = `${ALERT_TYPE_PREFIX}alert_type_cluster_state`; - -/** - * A listing of all alert types - */ -export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION, ALERT_TYPE_CLUSTER_STATE]; - -/** - * Matches the id for the built-in in email action type - * See x-pack/plugins/actions/server/builtin_action_types/email.ts - */ -export const ALERT_ACTION_TYPE_EMAIL = '.email'; - -/** - * The number of alerts that have been migrated - */ -export const NUMBER_OF_MIGRATED_ALERTS = 2; - -/** - * The advanced settings config name for the email address - */ -export const MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS = 'monitoring:alertingEmailAddress'; - -export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', 'ses', 'yahoo']; diff --git a/x-pack/legacy/plugins/monitoring/common/format_timestamp_to_duration.js b/x-pack/legacy/plugins/monitoring/common/format_timestamp_to_duration.js deleted file mode 100644 index 46c8f7db49b0f..0000000000000 --- a/x-pack/legacy/plugins/monitoring/common/format_timestamp_to_duration.js +++ /dev/null @@ -1,54 +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 moment from 'moment'; -import 'moment-duration-format'; -import { - FORMAT_DURATION_TEMPLATE_TINY, - FORMAT_DURATION_TEMPLATE_SHORT, - FORMAT_DURATION_TEMPLATE_LONG, - CALCULATE_DURATION_SINCE, - CALCULATE_DURATION_UNTIL, -} from './constants'; - -/* - * Formats a timestamp string - * @param timestamp: ISO time string - * @param calculationFlag: control "since" or "until" logic - * @param initialTime {Object} moment object (not required) - * @return string - */ -export function formatTimestampToDuration(timestamp, calculationFlag, initialTime) { - initialTime = initialTime || moment(); - let timeDuration; - if (calculationFlag === CALCULATE_DURATION_SINCE) { - timeDuration = moment.duration(initialTime - moment(timestamp)); // since: now - timestamp - } else if (calculationFlag === CALCULATE_DURATION_UNTIL) { - timeDuration = moment.duration(moment(timestamp) - initialTime); // until: timestamp - now - } else { - throw new Error( - '[formatTimestampToDuration] requires a [calculationFlag] parameter to specify format as "since" or "until" the given time.' - ); - } - - // See https://github.com/elastic/x-pack-kibana/issues/3554 - let duration; - if (Math.abs(initialTime.diff(timestamp, 'months')) >= 1) { - // time diff is greater than 1 month, show months / days - duration = moment.duration(timeDuration).format(FORMAT_DURATION_TEMPLATE_LONG); - } else if (Math.abs(initialTime.diff(timestamp, 'minutes')) >= 1) { - // time diff is less than 1 month but greater than a minute, show days / hours / minutes - duration = moment.duration(timeDuration).format(FORMAT_DURATION_TEMPLATE_SHORT); - } else { - // time diff is less than a minute, show seconds - duration = moment.duration(timeDuration).format(FORMAT_DURATION_TEMPLATE_TINY); - } - - return duration - .replace(/ 0 mins$/, '') - .replace(/ 0 hrs$/, '') - .replace(/ 0 days$/, ''); // See https://github.com/jsmreese/moment-duration-format/issues/64 -} diff --git a/x-pack/legacy/plugins/monitoring/common/formatting.js b/x-pack/legacy/plugins/monitoring/common/formatting.js deleted file mode 100644 index ed5d68f942dfd..0000000000000 --- a/x-pack/legacy/plugins/monitoring/common/formatting.js +++ /dev/null @@ -1,35 +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 moment from 'moment-timezone'; - -export const LARGE_FLOAT = '0,0.[00]'; -export const SMALL_FLOAT = '0.[00]'; -export const LARGE_BYTES = '0,0.0 b'; -export const SMALL_BYTES = '0.0 b'; -export const LARGE_ABBREVIATED = '0,0.[0]a'; - -/** - * Format the {@code date} in the user's expected date/time format using their dateFormat:tz defined time zone. - * @param date Either a numeric Unix timestamp or a {@code Date} object - * @returns The date formatted using 'LL LTS' - */ -export function formatDateTimeLocal(date, timezone) { - if (timezone === 'Browser') { - timezone = moment.tz.guess() || 'utc'; - } - - return moment.tz(date, timezone).format('LL LTS'); -} - -/** - * Shorten a Logstash Pipeline's hash for display purposes - * @param {string} hash The complete hash - * @return {string} The shortened hash - */ -export function shortenPipelineHash(hash) { - return hash.substr(0, 6); -} diff --git a/x-pack/legacy/plugins/monitoring/config.js b/x-pack/legacy/plugins/monitoring/config.ts similarity index 99% rename from x-pack/legacy/plugins/monitoring/config.js rename to x-pack/legacy/plugins/monitoring/config.ts index fd4e6512c5063..0c664fbe1c00c 100644 --- a/x-pack/legacy/plugins/monitoring/config.js +++ b/x-pack/legacy/plugins/monitoring/config.ts @@ -9,7 +9,7 @@ * @param {Object} Joi - HapiJS Joi module that allows for schema validation * @return {Object} config schema */ -export const config = Joi => { +export const config = (Joi: any) => { const DEFAULT_REQUEST_HEADERS = ['authorization']; return Joi.object({ diff --git a/x-pack/legacy/plugins/monitoring/index.js b/x-pack/legacy/plugins/monitoring/index.js deleted file mode 100644 index ccb45dc1f446f..0000000000000 --- a/x-pack/legacy/plugins/monitoring/index.js +++ /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 { get } from 'lodash'; -import { resolve } from 'path'; -import { config } from './config'; -import { getUiExports } from './ui_exports'; -import { KIBANA_ALERTING_ENABLED } from './common/constants'; - -/** - * Invokes plugin modules to instantiate the Monitoring plugin for Kibana - * @param kibana {Object} Kibana plugin instance - * @return {Object} Monitoring UI Kibana plugin object - */ -const deps = ['kibana', 'elasticsearch', 'xpack_main']; -if (KIBANA_ALERTING_ENABLED) { - deps.push(...['alerting', 'actions']); -} -export const monitoring = kibana => { - return new kibana.Plugin({ - require: deps, - id: 'monitoring', - configPrefix: 'monitoring', - publicDir: resolve(__dirname, 'public'), - init(server) { - const serverConfig = server.config(); - const npMonitoring = server.newPlatform.setup.plugins.monitoring; - if (npMonitoring) { - const kbnServerStatus = this.kbnServer.status; - npMonitoring.registerLegacyAPI({ - getServerStatus: () => { - const status = kbnServerStatus.toJSON(); - return get(status, 'overall.state'); - }, - }); - } - - server.injectUiAppVars('monitoring', () => { - return { - maxBucketSize: serverConfig.get('monitoring.ui.max_bucket_size'), - minIntervalSeconds: serverConfig.get('monitoring.ui.min_interval_seconds'), - kbnIndex: serverConfig.get('kibana.index'), - showLicenseExpiration: serverConfig.get('monitoring.ui.show_license_expiration'), - showCgroupMetricsElasticsearch: serverConfig.get( - 'monitoring.ui.container.elasticsearch.enabled' - ), - showCgroupMetricsLogstash: serverConfig.get('monitoring.ui.container.logstash.enabled'), // Note, not currently used, but see https://github.com/elastic/x-pack-kibana/issues/1559 part 2 - }; - }); - }, - config, - uiExports: getUiExports(), - }); -}; diff --git a/x-pack/legacy/plugins/monitoring/index.ts b/x-pack/legacy/plugins/monitoring/index.ts new file mode 100644 index 0000000000000..1a0fecb9ef5b5 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { config } from './config'; +import { KIBANA_ALERTING_ENABLED } from '../../../plugins/monitoring/common/constants'; + +/** + * Invokes plugin modules to instantiate the Monitoring plugin for Kibana + * @param kibana {Object} Kibana plugin instance + * @return {Object} Monitoring UI Kibana plugin object + */ +const deps = ['kibana', 'elasticsearch', 'xpack_main']; +if (KIBANA_ALERTING_ENABLED) { + deps.push(...['alerting', 'actions']); +} +export const monitoring = (kibana: any) => { + return new kibana.Plugin({ + require: deps, + id: 'monitoring', + configPrefix: 'monitoring', + init(server: Hapi.Server) { + const npMonitoring = server.newPlatform.setup.plugins.monitoring as object & { + registerLegacyAPI: (api: unknown) => void; + }; + if (npMonitoring) { + const kbnServerStatus = this.kbnServer.status; + npMonitoring.registerLegacyAPI({ + getServerStatus: () => { + const status = kbnServerStatus.toJSON(); + return status?.overall?.state; + }, + }); + } + }, + config, + }); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.test.js b/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.test.js deleted file mode 100644 index d8a6f1ad6bd9e..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.test.js +++ /dev/null @@ -1,76 +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 expect from '@kbn/expect'; -import { shallow } from 'enzyme'; -import { ChartTarget } from './chart_target'; - -const props = { - seriesToShow: ['Max Heap', 'Max Heap Used'], - series: [ - { - color: '#3ebeb0', - label: 'Max Heap', - id: 'Max Heap', - data: [ - [1562958960000, 1037959168], - [1562958990000, 1037959168], - [1562959020000, 1037959168], - ], - }, - { - color: '#3b73ac', - label: 'Max Heap Used', - id: 'Max Heap Used', - data: [ - [1562958960000, 639905768], - [1562958990000, 622312416], - [1562959020000, 555967504], - ], - }, - ], - timeRange: { - min: 1562958939851, - max: 1562962539851, - }, - hasLegend: true, - onBrush: () => void 0, - tickFormatter: () => void 0, - updateLegend: () => void 0, -}; - -jest.mock('../../np_imports/ui/chrome', () => { - return { - getBasePath: () => '', - }; -}); - -// TODO: Skipping for now, seems flaky in New Platform (needs more investigation) -describe.skip('Test legends to toggle series: ', () => { - const ids = props.series.map(item => item.id); - - describe('props.series: ', () => { - it('should toggle based on seriesToShow array', () => { - const component = shallow(); - - const componentClass = component.instance(); - - const seriesA = componentClass.filterData(props.series, [ids[0]]); - expect(seriesA.length).to.be(1); - expect(seriesA[0].id).to.be(ids[0]); - - const seriesB = componentClass.filterData(props.series, [ids[1]]); - expect(seriesB.length).to.be(1); - expect(seriesB[0].id).to.be(ids[1]); - - const seriesAB = componentClass.filterData(props.series, ids); - expect(seriesAB.length).to.be(2); - expect(seriesAB[0].id).to.be(ids[0]); - expect(seriesAB[1].id).to.be(ids[1]); - }); - }); -}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/license/index.js b/x-pack/legacy/plugins/monitoring/public/components/license/index.js deleted file mode 100644 index d43896d5f8d84..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/license/index.js +++ /dev/null @@ -1,91 +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 { - EuiPage, - EuiPageBody, - EuiSpacer, - EuiCodeBlock, - EuiPanel, - EuiText, - EuiLink, - EuiFlexGroup, - EuiFlexItem, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { LicenseStatus, AddLicense } from 'plugins/xpack_main/components'; -import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'plugins/monitoring/np_imports/ui/chrome'; - -const licenseManagement = `${chrome.getBasePath()}/app/kibana#/management/elasticsearch/license_management`; - -const LicenseUpdateInfoForPrimary = ({ isPrimaryCluster, uploadLicensePath }) => { - if (!isPrimaryCluster) { - return null; - } - - // viewed license is for the cluster directly connected to Kibana - return ; -}; - -const LicenseUpdateInfoForRemote = ({ isPrimaryCluster }) => { - if (isPrimaryCluster) { - return null; - } - - // viewed license is for a remote monitored cluster not directly connected to Kibana - return ( - -

- -

- - - {`curl -XPUT -u 'https://:/_license' -H 'Content-Type: application/json' -d @license.json`} - -
- ); -}; - -export function License(props) { - const { status, type, isExpired, expiryDate } = props; - return ( - - -

- -

-
- - - - - - - - - - - - - -

- For more license options please visit  - License Management. -

-
-
-
- ); -} diff --git a/x-pack/legacy/plugins/monitoring/public/directives/all.js b/x-pack/legacy/plugins/monitoring/public/directives/all.js deleted file mode 100644 index 43ad80a7a7e94..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/directives/all.js +++ /dev/null @@ -1,10 +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 './main'; -import './elasticsearch/ml_job_listing'; -import './beats/overview'; -import './beats/beat'; diff --git a/x-pack/legacy/plugins/monitoring/public/filters/index.js b/x-pack/legacy/plugins/monitoring/public/filters/index.js deleted file mode 100644 index a67770ff50dc8..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/filters/index.js +++ /dev/null @@ -1,30 +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 { capitalize } from 'lodash'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { formatNumber, formatMetric } from 'plugins/monitoring/lib/format_number'; -import { extractIp } from 'plugins/monitoring/lib/extract_ip'; - -const uiModule = uiModules.get('monitoring/filters', []); - -uiModule.filter('capitalize', function() { - return function(input) { - return capitalize(input.toLowerCase()); - }; -}); - -uiModule.filter('formatNumber', function() { - return formatNumber; -}); - -uiModule.filter('formatMetric', function() { - return formatMetric; -}); - -uiModule.filter('extractIp', function() { - return extractIp; -}); diff --git a/x-pack/legacy/plugins/monitoring/public/hacks/__tests__/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/monitoring/public/hacks/__tests__/toggle_app_link_in_nav.js deleted file mode 100644 index 6ed3371486740..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/hacks/__tests__/toggle_app_link_in_nav.js +++ /dev/null @@ -1,9 +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 { uiModules } from 'ui/modules'; - -uiModules.get('kibana').constant('monitoringUiEnabled', true); diff --git a/x-pack/legacy/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js deleted file mode 100644 index 9448e826f3723..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js +++ /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 { uiModules } from 'ui/modules'; -import { npStart } from 'ui/new_platform'; - -uiModules.get('monitoring/hacks').run(monitoringUiEnabled => { - if (monitoringUiEnabled) { - return; - } - - npStart.core.chrome.navLinks.update('monitoring', { hidden: true }); -}); diff --git a/x-pack/legacy/plugins/monitoring/public/icons/monitoring.svg b/x-pack/legacy/plugins/monitoring/public/icons/monitoring.svg deleted file mode 100644 index e00faca26a251..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/icons/monitoring.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - diff --git a/x-pack/legacy/plugins/monitoring/public/legacy.ts b/x-pack/legacy/plugins/monitoring/public/legacy.ts deleted file mode 100644 index 293b6ac7bd821..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/legacy.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import 'plugins/monitoring/filters'; -import 'plugins/monitoring/services/clusters'; -import 'plugins/monitoring/services/features'; -import 'plugins/monitoring/services/executor'; -import 'plugins/monitoring/services/license'; -import 'plugins/monitoring/services/title'; -import 'plugins/monitoring/services/breadcrumbs'; -import 'plugins/monitoring/directives/all'; -import 'plugins/monitoring/views/all'; -import { npSetup, npStart } from '../public/np_imports/legacy_imports'; -import { plugin } from './np_ready'; -import { localApplicationService } from '../../../../../src/legacy/core_plugins/kibana/public/local_application_service'; - -const pluginInstance = plugin({} as any); -pluginInstance.setup(npSetup.core, npSetup.plugins); -pluginInstance.start(npStart.core, { - ...npStart.plugins, - __LEGACY: { - localApplicationService, - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/angular_config.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/angular_config.ts deleted file mode 100644 index d1849d9247985..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/angular_config.ts +++ /dev/null @@ -1,157 +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 { - ICompileProvider, - IHttpProvider, - IHttpService, - ILocationProvider, - IModule, - IRootScopeService, -} from 'angular'; -import $ from 'jquery'; -import _, { cloneDeep, forOwn, get, set } from 'lodash'; -import * as Rx from 'rxjs'; -import { CoreStart, LegacyCoreStart } from 'kibana/public'; - -const isSystemApiRequest = (request: any) => - Boolean(request && request.headers && !!request.headers['kbn-system-api']); - -export const configureAppAngularModule = (angularModule: IModule, newPlatform: LegacyCoreStart) => { - const legacyMetadata = newPlatform.injectedMetadata.getLegacyMetadata(); - - forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => { - if (name !== undefined) { - // The legacy platform modifies some of these values, clone to an unfrozen object. - angularModule.value(name, cloneDeep(val)); - } - }); - - angularModule - .value('kbnVersion', newPlatform.injectedMetadata.getKibanaVersion()) - .value('buildNum', legacyMetadata.buildNum) - .value('buildSha', legacyMetadata.buildSha) - .value('serverName', legacyMetadata.serverName) - .value('esUrl', getEsUrl(newPlatform)) - .value('uiCapabilities', newPlatform.application.capabilities) - .config(setupCompileProvider(newPlatform)) - .config(setupLocationProvider()) - .config($setupXsrfRequestInterceptor(newPlatform)) - .run(capture$httpLoadingCount(newPlatform)) - .run($setupUICapabilityRedirect(newPlatform)); -}; - -const getEsUrl = (newPlatform: CoreStart) => { - const a = document.createElement('a'); - a.href = newPlatform.http.basePath.prepend('/elasticsearch'); - const protocolPort = /https/.test(a.protocol) ? 443 : 80; - const port = a.port || protocolPort; - return { - host: a.hostname, - port, - protocol: a.protocol, - pathname: a.pathname, - }; -}; - -const setupCompileProvider = (newPlatform: LegacyCoreStart) => ( - $compileProvider: ICompileProvider -) => { - if (!newPlatform.injectedMetadata.getLegacyMetadata().devMode) { - $compileProvider.debugInfoEnabled(false); - } -}; - -const setupLocationProvider = () => ($locationProvider: ILocationProvider) => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); - - $locationProvider.hashPrefix(''); -}; - -const $setupXsrfRequestInterceptor = (newPlatform: LegacyCoreStart) => { - const version = newPlatform.injectedMetadata.getLegacyMetadata().version; - - // Configure jQuery prefilter - $.ajaxPrefilter(({ kbnXsrfToken = true }: any, originalOptions, jqXHR) => { - if (kbnXsrfToken) { - jqXHR.setRequestHeader('kbn-version', version); - } - }); - - return ($httpProvider: IHttpProvider) => { - // Configure $httpProvider interceptor - $httpProvider.interceptors.push(() => { - return { - request(opts) { - const { kbnXsrfToken = true } = opts as any; - if (kbnXsrfToken) { - set(opts, ['headers', 'kbn-version'], version); - } - return opts; - }, - }; - }); - }; -}; - -/** - * Injected into angular module by ui/chrome angular integration - * and adds a root-level watcher that will capture the count of - * active $http requests on each digest loop and expose the count to - * the core.loadingCount api - * @param {Angular.Scope} $rootScope - * @param {HttpService} $http - * @return {undefined} - */ -const capture$httpLoadingCount = (newPlatform: CoreStart) => ( - $rootScope: IRootScopeService, - $http: IHttpService -) => { - newPlatform.http.addLoadingCountSource( - new Rx.Observable(observer => { - const unwatch = $rootScope.$watch(() => { - const reqs = $http.pendingRequests || []; - observer.next(reqs.filter(req => !isSystemApiRequest(req)).length); - }); - - return unwatch; - }) - ); -}; - -/** - * integrates with angular to automatically redirect to home if required - * capability is not met - */ -const $setupUICapabilityRedirect = (newPlatform: CoreStart) => ( - $rootScope: IRootScopeService, - $injector: any -) => { - const isKibanaAppRoute = window.location.pathname.endsWith('/app/kibana'); - // this feature only works within kibana app for now after everything is - // switched to the application service, this can be changed to handle all - // apps. - if (!isKibanaAppRoute) { - return; - } - $rootScope.$on( - '$routeChangeStart', - (event, { $$route: route }: { $$route?: { requireUICapability: boolean } } = {}) => { - if (!route || !route.requireUICapability) { - return; - } - - if (!get(newPlatform.application.capabilities, route.requireUICapability)) { - $injector.get('kbnUrl').change('/home'); - event.preventDefault(); - } - } - ); -}; diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/index.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/index.ts deleted file mode 100644 index 8fd8d170bbb40..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/index.ts +++ /dev/null @@ -1,48 +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 angular, { IModule } from 'angular'; - -import { AppMountContext, LegacyCoreStart } from 'kibana/public'; - -// @ts-ignore TODO: change to absolute path -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -// @ts-ignore TODO: change to absolute path -import chrome from 'plugins/monitoring/np_imports/ui/chrome'; -// @ts-ignore TODO: change to absolute path -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -// @ts-ignore TODO: change to absolute path -import { registerTimefilterWithGlobalState } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { configureAppAngularModule } from './angular_config'; - -import { localAppModule, appModuleName } from './modules'; - -export class AngularApp { - private injector?: angular.auto.IInjectorService; - - constructor({ core }: AppMountContext, { element }: { element: HTMLElement }) { - uiModules.addToModule(); - const app: IModule = localAppModule(core); - app.config(($routeProvider: any) => { - $routeProvider.eagerInstantiationEnabled(false); - uiRoutes.addToProvider($routeProvider); - }); - configureAppAngularModule(app, core as LegacyCoreStart); - registerTimefilterWithGlobalState(app); - const appElement = document.createElement('div'); - appElement.setAttribute('style', 'height: 100%'); - appElement.innerHTML = '
'; - this.injector = angular.bootstrap(appElement, [appModuleName]); - chrome.setInjector(this.injector); - angular.element(element).append(appElement); - } - - public destroy = () => { - if (this.injector) { - this.injector.get('$rootScope').$destroy(); - } - }; -} diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts deleted file mode 100644 index c6031cb220334..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts +++ /dev/null @@ -1,148 +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 angular, { IWindowService } from 'angular'; -// required for `ngSanitize` angular module -import 'angular-sanitize'; -import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; - -import { AppMountContext } from 'kibana/public'; -import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; -import { - createTopNavDirective, - createTopNavHelper, -} from '../../../../../../../src/plugins/kibana_legacy/public'; - -import { - GlobalStateProvider, - StateManagementConfigProvider, - AppStateProvider, - KbnUrlProvider, - npStart, -} from '../legacy_imports'; - -// @ts-ignore -import { PromiseServiceCreator } from './providers/promises'; -// @ts-ignore -import { PrivateProvider } from './providers/private'; -import { getSafeForExternalLink } from '../../lib/get_safe_for_external_link'; - -type IPrivate = (provider: (...injectable: any[]) => T) => T; - -export const appModuleName = 'monitoring'; -const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react']; - -export const localAppModule = (core: AppMountContext['core']) => { - createLocalI18nModule(); - createLocalPrivateModule(); - createLocalPromiseModule(); - createLocalStorage(); - createLocalConfigModule(core); - createLocalKbnUrlModule(); - createLocalStateModule(); - createLocalTopNavModule(npStart.plugins.navigation); - createHrefModule(core); - - const appModule = angular.module(appModuleName, [ - ...thirdPartyAngularDependencies, - 'monitoring/Config', - 'monitoring/I18n', - 'monitoring/Private', - 'monitoring/TopNav', - 'monitoring/State', - 'monitoring/Storage', - 'monitoring/href', - 'monitoring/services', - 'monitoring/filters', - 'monitoring/directives', - ]); - return appModule; -}; - -function createLocalStateModule() { - angular - .module('monitoring/State', [ - 'monitoring/Private', - 'monitoring/Config', - 'monitoring/KbnUrl', - 'monitoring/Promise', - ]) - .factory('AppState', function(Private: IPrivate) { - return Private(AppStateProvider); - }) - .service('globalState', function(Private: IPrivate) { - return Private(GlobalStateProvider); - }); -} - -function createLocalKbnUrlModule() { - angular - .module('monitoring/KbnUrl', ['monitoring/Private', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); -} - -function createLocalConfigModule(core: AppMountContext['core']) { - angular - .module('monitoring/Config', ['monitoring/Private']) - .provider('stateManagementConfig', StateManagementConfigProvider) - .provider('config', () => { - return { - $get: () => ({ - get: core.uiSettings.get.bind(core.uiSettings), - }), - }; - }); -} - -function createLocalPromiseModule() { - angular.module('monitoring/Promise', []).service('Promise', PromiseServiceCreator); -} - -function createLocalStorage() { - angular - .module('monitoring/Storage', []) - .service('localStorage', ($window: IWindowService) => new Storage($window.localStorage)) - .service('sessionStorage', ($window: IWindowService) => new Storage($window.sessionStorage)) - .service('sessionTimeout', () => {}); -} - -function createLocalPrivateModule() { - angular.module('monitoring/Private', []).provider('Private', PrivateProvider); -} - -function createLocalTopNavModule({ ui }: any) { - angular - .module('monitoring/TopNav', ['react']) - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(ui)); -} - -function createLocalI18nModule() { - angular - .module('monitoring/I18n', []) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); -} - -function createHrefModule(core: AppMountContext['core']) { - const name: string = 'kbnHref'; - angular.module('monitoring/href', []).directive(name, () => { - return { - restrict: 'A', - link: { - pre: (_$scope, _$el, $attr) => { - $attr.$observe(name, val => { - if (val) { - const url = getSafeForExternalLink(val as string); - $attr.$set('href', core.http.basePath.prepend(url)); - } - }); - }, - }, - }; - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/promises.js b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/promises.js deleted file mode 100644 index 22adccaf3db7f..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/promises.js +++ /dev/null @@ -1,116 +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'; - -export function PromiseServiceCreator($q, $timeout) { - function Promise(fn) { - if (typeof this === 'undefined') - throw new Error('Promise constructor must be called with "new"'); - - const defer = $q.defer(); - try { - fn(defer.resolve, defer.reject); - } catch (e) { - defer.reject(e); - } - return defer.promise; - } - - Promise.all = Promise.props = $q.all; - Promise.resolve = function(val) { - const defer = $q.defer(); - defer.resolve(val); - return defer.promise; - }; - Promise.reject = function(reason) { - const defer = $q.defer(); - defer.reject(reason); - return defer.promise; - }; - Promise.cast = $q.when; - Promise.delay = function(ms) { - return $timeout(_.noop, ms); - }; - Promise.method = function(fn) { - return function() { - const args = Array.prototype.slice.call(arguments); - return Promise.try(fn, args, this); - }; - }; - Promise.nodeify = function(promise, cb) { - promise.then(function(val) { - cb(void 0, val); - }, cb); - }; - Promise.map = function(arr, fn) { - return Promise.all( - arr.map(function(i, el, list) { - return Promise.try(fn, [i, el, list]); - }) - ); - }; - Promise.each = function(arr, fn) { - const queue = arr.slice(0); - let i = 0; - return (function next() { - if (!queue.length) return arr; - return Promise.try(fn, [arr.shift(), i++]).then(next); - })(); - }; - Promise.is = function(obj) { - // $q doesn't create instances of any constructor, promises are just objects with a then function - // https://github.com/angular/angular.js/blob/58f5da86645990ef984353418cd1ed83213b111e/src/ng/q.js#L335 - return obj && typeof obj.then === 'function'; - }; - Promise.halt = _.once(function() { - const promise = new Promise(() => {}); - promise.then = _.constant(promise); - promise.catch = _.constant(promise); - return promise; - }); - Promise.try = function(fn, args, ctx) { - if (typeof fn !== 'function') { - return Promise.reject(new TypeError('fn must be a function')); - } - - let value; - - if (Array.isArray(args)) { - try { - value = fn.apply(ctx, args); - } catch (e) { - return Promise.reject(e); - } - } else { - try { - value = fn.call(ctx, args); - } catch (e) { - return Promise.reject(e); - } - } - - return Promise.resolve(value); - }; - Promise.fromNode = function(takesCbFn) { - return new Promise(function(resolve, reject) { - takesCbFn(function(err, ...results) { - if (err) reject(err); - else if (results.length > 1) resolve(results); - else resolve(results[0]); - }); - }); - }; - Promise.race = function(iterable) { - return new Promise((resolve, reject) => { - for (const i of iterable) { - Promise.resolve(i).then(resolve, reject); - } - }); - }; - - return Promise; -} diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts deleted file mode 100644 index 208b7e2acdb0f..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts +++ /dev/null @@ -1,22 +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. - */ - -/** - * Last remaining 'ui/*' imports that will eventually be shimmed with their np alternatives - */ - -export { npSetup, npStart } from 'ui/new_platform'; -// @ts-ignore -export { GlobalStateProvider } from 'ui/state_management/global_state'; -// @ts-ignore -export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; -// @ts-ignore -export { AppStateProvider } from 'ui/state_management/app_state'; -// @ts-ignore -export { EventsProvider } from 'ui/events'; -// @ts-ignore -export { KbnUrlProvider } from 'ui/url'; -export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router'; diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/capabilities.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/ui/capabilities.ts deleted file mode 100644 index 5aff302501401..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/capabilities.ts +++ /dev/null @@ -1,8 +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 { npStart } from '../legacy_imports'; -export const capabilities = { get: () => npStart.core.application.capabilities }; diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/chrome.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/ui/chrome.ts deleted file mode 100644 index f0c5bacabecbf..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/chrome.ts +++ /dev/null @@ -1,33 +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 angular from 'angular'; -import { npStart, npSetup } from '../legacy_imports'; - -type OptionalInjector = void | angular.auto.IInjectorService; - -class Chrome { - private injector?: OptionalInjector; - - public setInjector = (injector: OptionalInjector): void => void (this.injector = injector); - public dangerouslyGetActiveInjector = (): OptionalInjector => this.injector; - - public getBasePath = (): string => npStart.core.http.basePath.get(); - - public getInjected = (name?: string, defaultValue?: any): string | unknown => { - const { getInjectedVar, getInjectedVars } = npSetup.core.injectedMetadata; - return name ? getInjectedVar(name, defaultValue) : getInjectedVars(); - }; - - public get breadcrumbs() { - const set = (...args: any[]) => npStart.core.chrome.setBreadcrumbs.apply(this, args as any); - return { set }; - } -} - -const chrome = new Chrome(); - -export default chrome; // eslint-disable-line import/no-default-export diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/modules.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/ui/modules.ts deleted file mode 100644 index 70201a7906110..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/modules.ts +++ /dev/null @@ -1,55 +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 angular from 'angular'; - -type PrivateProvider = (...args: any) => any; -interface Provider { - name: string; - provider: PrivateProvider; -} - -class Modules { - private _services: Provider[] = []; - private _filters: Provider[] = []; - private _directives: Provider[] = []; - - public get = (_name: string, _dep?: string[]) => { - return this; - }; - - public service = (...args: any) => { - this._services.push(args); - }; - - public filter = (...args: any) => { - this._filters.push(args); - }; - - public directive = (...args: any) => { - this._directives.push(args); - }; - - public addToModule = () => { - angular.module('monitoring/services', []); - angular.module('monitoring/filters', []); - angular.module('monitoring/directives', []); - - this._services.forEach(args => { - angular.module('monitoring/services').service.apply(null, args as any); - }); - - this._filters.forEach(args => { - angular.module('monitoring/filters').filter.apply(null, args as any); - }); - - this._directives.forEach(args => { - angular.module('monitoring/directives').directive.apply(null, args as any); - }); - }; -} - -export const uiModules = new Modules(); diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/timefilter.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/ui/timefilter.ts deleted file mode 100644 index e28699bd126b9..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/timefilter.ts +++ /dev/null @@ -1,31 +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 { IModule, IRootScopeService } from 'angular'; -import { npStart, registerTimefilterWithGlobalStateFactory } from '../legacy_imports'; - -const { - core: { uiSettings }, -} = npStart; -export const { timefilter } = npStart.plugins.data.query.timefilter; - -uiSettings.overrideLocalDefault( - 'timepicker:refreshIntervalDefaults', - JSON.stringify({ value: 10000, pause: false }) -); -uiSettings.overrideLocalDefault( - 'timepicker:timeDefaults', - JSON.stringify({ from: 'now-1h', to: 'now' }) -); - -export const registerTimefilterWithGlobalState = (app: IModule) => { - app.run((globalState: any, $rootScope: IRootScopeService) => { - globalState.fetch(); - globalState.$inheritedGlobalState = true; - globalState.save(); - registerTimefilterWithGlobalStateFactory(timefilter, globalState, $rootScope); - }); -}; diff --git a/x-pack/legacy/plugins/monitoring/public/np_ready/plugin.ts b/x-pack/legacy/plugins/monitoring/public/np_ready/plugin.ts deleted file mode 100644 index 5598a7a51cf42..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_ready/plugin.ts +++ /dev/null @@ -1,28 +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 { App, CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; - -export class MonitoringPlugin implements Plugin { - constructor(ctx: PluginInitializerContext) {} - - public setup(core: CoreSetup, plugins: any) { - const app: App = { - id: 'monitoring', - title: 'Monitoring', - mount: async (context, params) => { - const { AngularApp } = await import('../np_imports/angular'); - const monitoringApp = new AngularApp(context, params); - return monitoringApp.destroy; - }, - }; - - core.application.register(app); - } - - public start(core: CoreStart, plugins: any) {} - public stop() {} -} diff --git a/x-pack/legacy/plugins/monitoring/public/register_feature.ts b/x-pack/legacy/plugins/monitoring/public/register_feature.ts deleted file mode 100644 index 9b72e01a19394..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/register_feature.ts +++ /dev/null @@ -1,30 +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 { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; -import { npSetup } from 'ui/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; - -const { - plugins: { home }, -} = npSetup; - -if (chrome.getInjected('monitoringUiEnabled')) { - home.featureCatalogue.register({ - id: 'monitoring', - title: i18n.translate('xpack.monitoring.monitoringTitle', { - defaultMessage: 'Monitoring', - }), - description: i18n.translate('xpack.monitoring.monitoringDescription', { - defaultMessage: 'Track the real-time health and performance of your Elastic Stack.', - }), - icon: 'monitoringApp', - path: '/app/monitoring', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs.js b/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs.js deleted file mode 100644 index d0fe600386307..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs.js +++ /dev/null @@ -1,10 +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 { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { breadcrumbsProvider } from './breadcrumbs_provider'; -const uiModule = uiModules.get('monitoring/breadcrumbs', []); -uiModule.service('breadcrumbs', breadcrumbsProvider); diff --git a/x-pack/legacy/plugins/monitoring/public/services/executor.js b/x-pack/legacy/plugins/monitoring/public/services/executor.js deleted file mode 100644 index 5004cd0238012..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/services/executor.js +++ /dev/null @@ -1,10 +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 { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { executorProvider } from './executor_provider'; -const uiModule = uiModules.get('monitoring/executor', []); -uiModule.service('$executor', executorProvider); diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/instance/index.js b/x-pack/legacy/plugins/monitoring/public/views/kibana/instance/index.js deleted file mode 100644 index 6535bd7410445..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/kibana/instance/index.js +++ /dev/null @@ -1,153 +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. - */ - -/* - * Kibana Instance - */ -import React from 'react'; -import { get } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiSpacer, - EuiFlexGrid, - EuiFlexItem, - EuiPanel, -} from '@elastic/eui'; -import { MonitoringTimeseriesContainer } from '../../../components/chart'; -import { DetailStatus } from 'plugins/monitoring/components/kibana/detail_status'; -import { I18nContext } from 'ui/i18n'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_KIBANA } from '../../../../common/constants'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const $route = $injector.get('$route'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana/${$route.current.params.uuid}`; - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/kibana/instances/:uuid', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_KIBANA] }); - }, - pageData: getPageData, - }, - controllerAs: 'monitoringKibanaInstanceApp', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - title: `Kibana - ${get($scope.pageData, 'kibanaSummary.name')}`, - defaultData: {}, - getPageData, - reactNodeId: 'monitoringKibanaInstanceApp', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - data => { - if (!data || !data.metrics) { - return; - } - - this.setTitle(`Kibana - ${get(data, 'kibanaSummary.name')}`); - - this.renderReact( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/loading/index.html b/x-pack/legacy/plugins/monitoring/public/views/loading/index.html deleted file mode 100644 index 11da26a0ceed2..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/loading/index.html +++ /dev/null @@ -1,5 +0,0 @@ - -
-
-
-
diff --git a/x-pack/legacy/plugins/monitoring/public/views/loading/index.js b/x-pack/legacy/plugins/monitoring/public/views/loading/index.js deleted file mode 100644 index 0488683845a7d..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/loading/index.js +++ /dev/null @@ -1,51 +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 { render, unmountComponentAtNode } from 'react-dom'; -import { PageLoading } from 'plugins/monitoring/components'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { I18nContext } from 'ui/i18n'; -import template from './index.html'; -import { CODE_PATH_LICENSE } from '../../../common/constants'; - -const REACT_DOM_ID = 'monitoringLoadingReactApp'; - -uiRoutes.when('/loading', { - template, - controller: class { - constructor($injector, $scope) { - const monitoringClusters = $injector.get('monitoringClusters'); - const kbnUrl = $injector.get('kbnUrl'); - - $scope.$on('$destroy', () => { - unmountComponentAtNode(document.getElementById(REACT_DOM_ID)); - }); - - $scope.$$postDigest(() => { - this.renderReact(); - }); - - monitoringClusters(undefined, undefined, [CODE_PATH_LICENSE]).then(clusters => { - if (clusters && clusters.length) { - kbnUrl.changePath('/home'); - return; - } - kbnUrl.changePath('/no-data'); - return; - }); - } - - renderReact() { - render( - - - , - document.getElementById(REACT_DOM_ID) - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/ui_exports.js b/x-pack/legacy/plugins/monitoring/ui_exports.js deleted file mode 100644 index e0c04411ef46b..0000000000000 --- a/x-pack/legacy/plugins/monitoring/ui_exports.js +++ /dev/null @@ -1,65 +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 { i18n } from '@kbn/i18n'; -import { resolve } from 'path'; -import { - MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, - KIBANA_ALERTING_ENABLED, -} from './common/constants'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; - -/** - * Configuration of dependency objects for the UI, which are needed for the - * Monitoring UI app and views and data for outside the monitoring - * app (injectDefaultVars and hacks) - * @return {Object} data per Kibana plugin uiExport schema - */ -export const getUiExports = () => { - const uiSettingDefaults = {}; - if (KIBANA_ALERTING_ENABLED) { - uiSettingDefaults[MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS] = { - name: i18n.translate('xpack.monitoring.alertingEmailAddress.name', { - defaultMessage: 'Alerting email address', - }), - value: '', - description: i18n.translate('xpack.monitoring.alertingEmailAddress.description', { - defaultMessage: `The default email address to receive alerts from Stack Monitoring`, - }), - category: ['monitoring'], - }; - } - - return { - app: { - title: i18n.translate('xpack.monitoring.stackMonitoringTitle', { - defaultMessage: 'Stack Monitoring', - }), - order: 9002, - description: i18n.translate('xpack.monitoring.uiExportsDescription', { - defaultMessage: 'Monitoring for Elastic Stack', - }), - icon: 'plugins/monitoring/icons/monitoring.svg', - euiIconType: 'monitoringApp', - linkToLastSubUrl: false, - main: 'plugins/monitoring/legacy', - category: DEFAULT_APP_CATEGORIES.management, - }, - injectDefaultVars(server) { - const config = server.config(); - return { - monitoringUiEnabled: config.get('monitoring.ui.enabled'), - monitoringLegacyEmailAddress: config.get( - 'monitoring.cluster_alerts.email_notifications.email_address' - ), - }; - }, - uiSettingDefaults, - hacks: ['plugins/monitoring/hacks/toggle_app_link_in_nav'], - home: ['plugins/monitoring/register_feature'], - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }; -}; diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js index c830fc9fcd483..99071a2f85e13 100644 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js +++ b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js @@ -6,7 +6,7 @@ import { boomify } from 'boom'; import { get } from 'lodash'; -import { KIBANA_SETTINGS_TYPE } from '../../../../../monitoring/common/constants'; +import { KIBANA_SETTINGS_TYPE } from '../../../../../../../plugins/monitoring/common/constants'; const getClusterUuid = async callCluster => { const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid' }); diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index bbdf1a2e7cb76..115cc08871ea4 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -3,8 +3,8 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["monitoring"], - "requiredPlugins": ["licensing", "features"], - "optionalPlugins": ["alerting", "actions", "infra", "telemetryCollectionManager", "usageCollection"], + "requiredPlugins": ["licensing", "features", "data", "navigation", "kibanaLegacy"], + "optionalPlugins": ["alerting", "actions", "infra", "telemetryCollectionManager", "usageCollection", "home"], "server": true, - "ui": false + "ui": true } diff --git a/x-pack/legacy/plugins/monitoring/public/_hacks.scss b/x-pack/plugins/monitoring/public/_hacks.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/_hacks.scss rename to x-pack/plugins/monitoring/public/_hacks.scss diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts new file mode 100644 index 0000000000000..3fa79cedf4ce7 --- /dev/null +++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts @@ -0,0 +1,248 @@ +/* + * 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 angular, { IWindowService } from 'angular'; +import '../views/all'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; +import 'angular-route'; +import '../index.scss'; +import { capitalize } from 'lodash'; +import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; +import { AppMountContext } from 'kibana/public'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { + createTopNavDirective, + createTopNavHelper, +} from '../../../../../src/plugins/kibana_legacy/public'; +import { MonitoringPluginDependencies } from '../types'; +import { GlobalState } from '../url_state'; +import { getSafeForExternalLink } from '../lib/get_safe_for_external_link'; + +// @ts-ignore +import { formatNumber, formatMetric } from '../lib/format_number'; +// @ts-ignore +import { extractIp } from '../lib/extract_ip'; +// @ts-ignore +import { PrivateProvider } from './providers/private'; +// @ts-ignore +import { KbnUrlProvider } from './providers/url'; +// @ts-ignore +import { breadcrumbsProvider } from '../services/breadcrumbs'; +// @ts-ignore +import { monitoringClustersProvider } from '../services/clusters'; +// @ts-ignore +import { executorProvider } from '../services/executor'; +// @ts-ignore +import { featuresProvider } from '../services/features'; +// @ts-ignore +import { licenseProvider } from '../services/license'; +// @ts-ignore +import { titleProvider } from '../services/title'; +// @ts-ignore +import { monitoringBeatsBeatProvider } from '../directives/beats/beat'; +// @ts-ignore +import { monitoringBeatsOverviewProvider } from '../directives/beats/overview'; +// @ts-ignore +import { monitoringMlListingProvider } from '../directives/elasticsearch/ml_job_listing'; +// @ts-ignore +import { monitoringMainProvider } from '../directives/main'; + +export const appModuleName = 'monitoring'; + +type IPrivate = (provider: (...injectable: unknown[]) => T) => T; + +const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'ui.bootstrap']; + +export const localAppModule = ({ + core, + data: { query }, + navigation, + externalConfig, +}: MonitoringPluginDependencies) => { + createLocalI18nModule(); + createLocalPrivateModule(); + createLocalStorage(); + createLocalConfigModule(core); + createLocalKbnUrlModule(); + createLocalStateModule(query); + createLocalTopNavModule(navigation); + createHrefModule(core); + createMonitoringAppServices(); + createMonitoringAppDirectives(); + createMonitoringAppConfigConstants(externalConfig); + createMonitoringAppFilters(); + + const appModule = angular.module(appModuleName, [ + ...thirdPartyAngularDependencies, + 'monitoring/I18n', + 'monitoring/Private', + 'monitoring/KbnUrl', + 'monitoring/Storage', + 'monitoring/Config', + 'monitoring/State', + 'monitoring/TopNav', + 'monitoring/href', + 'monitoring/constants', + 'monitoring/services', + 'monitoring/filters', + 'monitoring/directives', + ]); + return appModule; +}; + +function createMonitoringAppConfigConstants(keys: MonitoringPluginDependencies['externalConfig']) { + let constantsModule = angular.module('monitoring/constants', []); + keys.map(([key, value]) => (constantsModule = constantsModule.constant(key as string, value))); +} + +function createLocalStateModule(query: any) { + angular + .module('monitoring/State', ['monitoring/Private']) + .service('globalState', function( + Private: IPrivate, + $rootScope: ng.IRootScopeService, + $location: ng.ILocationService + ) { + function GlobalStateProvider(this: any) { + const state = new GlobalState(query, $rootScope, $location, this); + const initialState: any = state.getState(); + for (const key in initialState) { + if (!initialState.hasOwnProperty(key)) { + continue; + } + this[key] = initialState[key]; + } + this.save = () => { + const newState = { ...this }; + delete newState.save; + state.setState(newState); + }; + } + return Private(GlobalStateProvider); + }); +} + +function createLocalKbnUrlModule() { + angular + .module('monitoring/KbnUrl', ['monitoring/Private', 'ngRoute']) + .service('kbnUrl', function(Private: IPrivate) { + return Private(KbnUrlProvider); + }); +} + +function createMonitoringAppServices() { + angular + .module('monitoring/services', ['monitoring/Private']) + .service('breadcrumbs', function(Private: IPrivate) { + return Private(breadcrumbsProvider); + }) + .service('monitoringClusters', function(Private: IPrivate) { + return Private(monitoringClustersProvider); + }) + .service('$executor', function(Private: IPrivate) { + return Private(executorProvider); + }) + .service('features', function(Private: IPrivate) { + return Private(featuresProvider); + }) + .service('license', function(Private: IPrivate) { + return Private(licenseProvider); + }) + .service('title', function(Private: IPrivate) { + return Private(titleProvider); + }); +} + +function createMonitoringAppDirectives() { + angular + .module('monitoring/directives', []) + .directive('monitoringBeatsBeat', monitoringBeatsBeatProvider) + .directive('monitoringBeatsOverview', monitoringBeatsOverviewProvider) + .directive('monitoringMlListing', monitoringMlListingProvider) + .directive('monitoringMain', monitoringMainProvider); +} + +function createMonitoringAppFilters() { + angular + .module('monitoring/filters', []) + .filter('capitalize', function() { + return function(input: string) { + return capitalize(input?.toLowerCase()); + }; + }) + .filter('formatNumber', function() { + return formatNumber; + }) + .filter('formatMetric', function() { + return formatMetric; + }) + .filter('extractIp', function() { + return extractIp; + }); +} + +function createLocalConfigModule(core: MonitoringPluginDependencies['core']) { + angular.module('monitoring/Config', []).provider('config', function() { + return { + $get: () => ({ + get: (key: string) => core.uiSettings?.get(key), + }), + }; + }); +} + +function createLocalStorage() { + angular + .module('monitoring/Storage', []) + .service('localStorage', function($window: IWindowService) { + return new Storage($window.localStorage); + }) + .service('sessionStorage', function($window: IWindowService) { + return new Storage($window.sessionStorage); + }) + .service('sessionTimeout', function() { + return {}; + }); +} + +function createLocalPrivateModule() { + angular.module('monitoring/Private', []).provider('Private', PrivateProvider); +} + +function createLocalTopNavModule({ ui }: MonitoringPluginDependencies['navigation']) { + angular + .module('monitoring/TopNav', ['react']) + .directive('kbnTopNav', createTopNavDirective) + .directive('kbnTopNavHelper', createTopNavHelper(ui)); +} + +function createLocalI18nModule() { + angular + .module('monitoring/I18n', []) + .provider('i18n', I18nProvider) + .filter('i18n', i18nFilter) + .directive('i18nId', i18nDirective); +} + +function createHrefModule(core: AppMountContext['core']) { + const name: string = 'kbnHref'; + angular.module('monitoring/href', []).directive(name, function() { + return { + restrict: 'A', + link: { + pre: (_$scope, _$el, $attr) => { + $attr.$observe(name, val => { + if (val) { + const url = getSafeForExternalLink(val as string); + $attr.$set('href', core.http.basePath.prepend(url)); + } + }); + }, + }, + }; + }); +} diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/routes.ts b/x-pack/plugins/monitoring/public/angular/helpers/routes.ts similarity index 62% rename from x-pack/legacy/plugins/monitoring/public/np_imports/ui/routes.ts rename to x-pack/plugins/monitoring/public/angular/helpers/routes.ts index 22da56a8d184a..b9307e8594a7a 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/routes.ts +++ b/x-pack/plugins/monitoring/public/angular/helpers/routes.ts @@ -4,36 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -type RouteObject = [string, any]; +type RouteObject = [string, { reloadOnSearch: boolean }]; interface Redirect { redirectTo: string; } class Routes { - private _routes: RouteObject[] = []; - private _redirect?: Redirect; + private routes: RouteObject[] = []; + public redirect?: Redirect = { redirectTo: '/no-data' }; public when = (...args: RouteObject) => { const [, routeOptions] = args; routeOptions.reloadOnSearch = false; - this._routes.push(args); + this.routes.push(args); return this; }; public otherwise = (redirect: Redirect) => { - this._redirect = redirect; + this.redirect = redirect; return this; }; public addToProvider = ($routeProvider: any) => { - this._routes.forEach(args => { + this.routes.forEach(args => { $routeProvider.when.apply(this, args); }); - if (this._redirect) { - $routeProvider.otherwise(this._redirect); + if (this.redirect) { + $routeProvider.otherwise(this.redirect); } }; } -const uiRoutes = new Routes(); -export default uiRoutes; // eslint-disable-line import/no-default-export +export const uiRoutes = new Routes(); diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/utils.ts b/x-pack/plugins/monitoring/public/angular/helpers/utils.ts similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/np_imports/ui/utils.ts rename to x-pack/plugins/monitoring/public/angular/helpers/utils.ts diff --git a/x-pack/plugins/monitoring/public/angular/index.ts b/x-pack/plugins/monitoring/public/angular/index.ts new file mode 100644 index 0000000000000..b371503fdb7c9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/angular/index.ts @@ -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 angular, { IModule } from 'angular'; +import { uiRoutes } from './helpers/routes'; +import { Legacy } from '../legacy_shims'; +import { configureAppAngularModule } from '../../../../../src/plugins/kibana_legacy/public'; +import { localAppModule, appModuleName } from './app_modules'; + +import { MonitoringPluginDependencies } from '../types'; + +const APP_WRAPPER_CLASS = 'monitoringApplicationWrapper'; +export class AngularApp { + private injector?: angular.auto.IInjectorService; + + constructor(deps: MonitoringPluginDependencies) { + const { + core, + element, + data, + navigation, + isCloud, + pluginInitializerContext, + externalConfig, + } = deps; + const app: IModule = localAppModule(deps); + app.run(($injector: angular.auto.IInjectorService) => { + this.injector = $injector; + Legacy.init( + { core, element, data, navigation, isCloud, pluginInitializerContext, externalConfig }, + this.injector + ); + }); + + app.config(($routeProvider: unknown) => uiRoutes.addToProvider($routeProvider)); + + const np = { core, env: pluginInitializerContext.env }; + configureAppAngularModule(app, np, true); + const appElement = document.createElement('div'); + appElement.setAttribute('style', 'height: 100%'); + appElement.innerHTML = '
'; + + if (!element.classList.contains(APP_WRAPPER_CLASS)) { + element.classList.add(APP_WRAPPER_CLASS); + } + + angular.bootstrap(appElement, [appModuleName]); + angular.element(element).append(appElement); + } + + public destroy = () => { + if (this.injector) { + this.injector.get('$rootScope').$destroy(); + } + }; + + public applyScope = () => { + if (!this.injector) { + return; + } + + const rootScope = this.injector.get('$rootScope'); + rootScope.$applyAsync(); + }; +} diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/private.js b/x-pack/plugins/monitoring/public/angular/providers/private.js similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/private.js rename to x-pack/plugins/monitoring/public/angular/providers/private.js index 6eae978b828b3..e456f2617f7b8 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/private.js +++ b/x-pack/plugins/monitoring/public/angular/providers/private.js @@ -193,4 +193,6 @@ export function PrivateProvider() { return Private; }, ]; + + return provider; } diff --git a/x-pack/plugins/monitoring/public/angular/providers/url.js b/x-pack/plugins/monitoring/public/angular/providers/url.js new file mode 100644 index 0000000000000..57b63708b546e --- /dev/null +++ b/x-pack/plugins/monitoring/public/angular/providers/url.js @@ -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 _ from 'lodash'; + +export function KbnUrlProvider($injector, $location, $rootScope, $parse) { + /** + * the `kbnUrl` service was created to smooth over some of the + * inconsistent behavior that occurs when modifying the url via + * the `$location` api. In general it is recommended that you use + * the `kbnUrl` service any time you want to modify the url. + * + * "features" that `kbnUrl` does it's best to guarantee, which + * are not guaranteed with the `$location` service: + * - calling `kbnUrl.change()` with a url that resolves to the current + * route will force a full transition (rather than just updating the + * properties of the $route object) + * + * Additional features of `kbnUrl` + * - parameterized urls + * - easily include an app state with the url + * + * @type {KbnUrl} + */ + const self = this; + + /** + * Navigate to a url + * + * @param {String} url - the new url, can be a template. See #eval + * @param {Object} [paramObj] - optional set of parameters for the url template + * @return {undefined} + */ + self.change = function(url, paramObj, appState) { + self._changeLocation('url', url, paramObj, false, appState); + }; + + /** + * Same as #change except only changes the url's path, + * leaving the search string and such intact + * + * @param {String} path - the new path, can be a template. See #eval + * @param {Object} [paramObj] - optional set of parameters for the path template + * @return {undefined} + */ + self.changePath = function(path, paramObj) { + self._changeLocation('path', path, paramObj); + }; + + /** + * Same as #change except that it removes the current url from history + * + * @param {String} url - the new url, can be a template. See #eval + * @param {Object} [paramObj] - optional set of parameters for the url template + * @return {undefined} + */ + self.redirect = function(url, paramObj, appState) { + self._changeLocation('url', url, paramObj, true, appState); + }; + + /** + * Same as #redirect except only changes the url's path, + * leaving the search string and such intact + * + * @param {String} path - the new path, can be a template. See #eval + * @param {Object} [paramObj] - optional set of parameters for the path template + * @return {undefined} + */ + self.redirectPath = function(path, paramObj) { + self._changeLocation('path', path, paramObj, true); + }; + + /** + * Evaluate a url template. templates can contain double-curly wrapped + * expressions that are evaluated in the context of the paramObj + * + * @param {String} template - the url template to evaluate + * @param {Object} [paramObj] - the variables to expose to the template + * @return {String} - the evaluated result + * @throws {Error} If any of the expressions can't be parsed. + */ + self.eval = function(template, paramObj) { + paramObj = paramObj || {}; + + return template.replace(/\{\{([^\}]+)\}\}/g, function(match, expr) { + // remove filters + const key = expr.split('|')[0].trim(); + + // verify that the expression can be evaluated + const p = $parse(key)(paramObj); + + // if evaluation can't be made, throw + if (_.isUndefined(p)) { + throw new Error(`Replacement failed, unresolved expression: ${expr}`); + } + + return encodeURIComponent($parse(expr)(paramObj)); + }); + }; + + /** + * convert an object's route to an href, compatible with + * window.location.href= and + * + * @param {Object} obj - any object that list's it's routes at obj.routes{} + * @param {string} route - the route name + * @return {string} - the computed href + */ + self.getRouteHref = function(obj, route) { + return '#' + self.getRouteUrl(obj, route); + }; + + /** + * convert an object's route to a url, compatible with url.change() or $location.url() + * + * @param {Object} obj - any object that list's it's routes at obj.routes{} + * @param {string} route - the route name + * @return {string} - the computed url + */ + self.getRouteUrl = function(obj, route) { + const template = obj && obj.routes && obj.routes[route]; + if (template) return self.eval(template, obj); + }; + + /** + * Similar to getRouteUrl, supports objects which list their routes, + * and redirects to the named route. See #redirect + * + * @param {Object} obj - any object that list's it's routes at obj.routes{} + * @param {string} route - the route name + * @return {undefined} + */ + self.redirectToRoute = function(obj, route) { + self.redirect(self.getRouteUrl(obj, route)); + }; + + /** + * Similar to getRouteUrl, supports objects which list their routes, + * and changes the url to the named route. See #change + * + * @param {Object} obj - any object that list's it's routes at obj.routes{} + * @param {string} route - the route name + * @return {undefined} + */ + self.changeToRoute = function(obj, route) { + self.change(self.getRouteUrl(obj, route)); + }; + + /** + * Removes the given parameter from the url. Does so without modifying the browser + * history. + * @param param + */ + self.removeParam = function(param) { + $location.search(param, null).replace(); + }; + + ///// + // private api + ///// + let reloading; + + self._changeLocation = function(type, url, paramObj, replace, appState) { + const prev = { + path: $location.path(), + search: $location.search(), + }; + + url = self.eval(url, paramObj); + $location[type](url); + if (replace) $location.replace(); + + if (appState) { + $location.search(appState.getQueryParamName(), appState.toQueryParam()); + } + + const next = { + path: $location.path(), + search: $location.search(), + }; + + if ($injector.has('$route')) { + const $route = $injector.get('$route'); + + if (self._shouldForceReload(next, prev, $route)) { + reloading = $rootScope.$on('$locationChangeSuccess', function() { + // call the "unlisten" function returned by $on + reloading(); + reloading = false; + + $route.reload(); + }); + } + } + }; + + // determine if the router will automatically reload the route + self._shouldForceReload = function(next, prev, $route) { + if (reloading) return false; + + const route = $route.current && $route.current.$$route; + if (!route) return false; + + // for the purposes of determining whether the router will + // automatically be reloading, '' and '/' are equal + const nextPath = next.path || '/'; + const prevPath = prev.path || '/'; + if (nextPath !== prevPath) return false; + + const reloadOnSearch = route.reloadOnSearch; + const searchSame = _.isEqual(next.search, prev.search); + return (reloadOnSearch && searchSame) || !reloadOnSearch; + }; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap rename to x-pack/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/__tests__/map_severity.js b/x-pack/plugins/monitoring/public/components/alerts/__tests__/map_severity.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/__tests__/map_severity.js rename to x-pack/plugins/monitoring/public/components/alerts/__tests__/map_severity.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js b/x-pack/plugins/monitoring/public/components/alerts/alerts.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js rename to x-pack/plugins/monitoring/public/components/alerts/alerts.js index 95c1af5549198..a86fdb1041a5c 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js +++ b/x-pack/plugins/monitoring/public/components/alerts/alerts.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import chrome from '../../np_imports/ui/chrome'; +import { Legacy } from '../../legacy_shims'; import { capitalize, get } from 'lodash'; import { formatDateTimeLocal } from '../../../common/formatting'; import { formatTimestampToDuration } from '../../../common'; @@ -16,8 +16,8 @@ import { ALERT_TYPE_CLUSTER_STATE, } from '../../../common/constants'; import { mapSeverity } from './map_severity'; -import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; -import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; +import { FormattedAlert } from '../../components/alerts/formatted_alert'; +import { EuiMonitoringTable } from '../../components/table'; import { EuiHealth, EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -162,7 +162,7 @@ export const Alerts = ({ alerts, angular, sorting, pagination, onTableChange }) category: get(alert, 'metadata.link', get(alert, 'type', null)), })); - const injector = chrome.dangerouslyGetActiveInjector(); + const injector = Legacy.shims.getAngularInjector(); const timezone = injector.get('config').get('dateFormat:tz'); return ( diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap rename to x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap rename to x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap rename to x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap rename to x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx similarity index 91% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx index 6b7e2391e0301..2c2d7c6464e1b 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx @@ -7,11 +7,15 @@ import React from 'react'; import { mockUseEffects } from '../../../jest.helpers'; import { shallow, ShallowWrapper } from 'enzyme'; -import { kfetch } from 'ui/kfetch'; +import { Legacy } from '../../../legacy_shims'; import { AlertsConfiguration, AlertsConfigurationProps } from './configuration'; -jest.mock('ui/kfetch', () => ({ - kfetch: jest.fn(), +jest.mock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: jest.fn(), + }, + }, })); const defaultProps: AlertsConfigurationProps = { @@ -61,7 +65,7 @@ describe('Configuration', () => { beforeEach(async () => { mockUseEffects(2); - (kfetch as jest.Mock).mockImplementation(() => { + (Legacy.shims.kfetch as jest.Mock).mockImplementation(() => { return { data: [ { @@ -101,7 +105,7 @@ describe('Configuration', () => { describe('edit action', () => { let component: ShallowWrapper; beforeEach(async () => { - (kfetch as jest.Mock).mockImplementation(() => { + (Legacy.shims.kfetch as jest.Mock).mockImplementation(() => { return { data: [], }; @@ -124,7 +128,7 @@ describe('Configuration', () => { describe('no email address', () => { let component: ShallowWrapper; beforeEach(async () => { - (kfetch as jest.Mock).mockImplementation(() => { + (Legacy.shims.kfetch as jest.Mock).mockImplementation(() => { return { data: [ { diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx similarity index 96% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx index eaa474ba177b1..61f86b0f9b609 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx @@ -5,10 +5,10 @@ */ import React, { ReactNode } from 'react'; -import { kfetch } from 'ui/kfetch'; import { EuiSteps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ActionResult } from '../../../../../../../plugins/actions/common'; +import { Legacy } from '../../../legacy_shims'; +import { ActionResult } from '../../../../../../plugins/actions/common'; import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; import { getMissingFieldErrors } from '../../../lib/form_validation'; import { Step1 } from './step1'; @@ -59,7 +59,7 @@ export const AlertsConfiguration: React.FC = ( }, [emailAddress]); async function fetchEmailActions() { - const kibanaActions = await kfetch({ + const kibanaActions = await Legacy.shims.kfetch({ method: 'GET', pathname: `/api/action/_getAll`, }); @@ -84,7 +84,7 @@ export const AlertsConfiguration: React.FC = ( setShowFormErrors(false); try { - await kfetch({ + await Legacy.shims.kfetch({ method: 'POST', pathname: `/api/monitoring/v1/alerts`, body: JSON.stringify({ selectedEmailActionId, emailAddress }), diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts b/x-pack/plugins/monitoring/public/components/alerts/configuration/index.ts similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts rename to x-pack/plugins/monitoring/public/components/alerts/configuration/index.ts diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx similarity index 83% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx index 19a1a61d00a42..5734d379dfb0c 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx @@ -49,9 +49,13 @@ describe('Step1', () => { beforeEach(() => { jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch: () => { - return {}; + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: () => { + return {}; + }, + }, }, })); setModules(); @@ -97,8 +101,12 @@ describe('Step1', () => { it('should send up the create to the server', async () => { const kfetch = jest.fn().mockImplementation(() => {}); jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch, + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch, + }, + }, })); setModules(); }); @@ -152,8 +160,12 @@ describe('Step1', () => { it('should send up the edit to the server', async () => { const kfetch = jest.fn().mockImplementation(() => {}); jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch, + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch, + }, + }, })); setModules(); }); @@ -194,13 +206,17 @@ describe('Step1', () => { describe('testing', () => { it('should allow for testing', async () => { jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch: jest.fn().mockImplementation(arg => { - if (arg.pathname === '/api/action/1/_execute') { - return { status: 'ok' }; - } - return {}; - }), + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: jest.fn().mockImplementation(arg => { + if (arg.pathname === '/api/action/1/_execute') { + return { status: 'ok' }; + } + return {}; + }), + }, + }, })); setModules(); }); @@ -234,12 +250,16 @@ describe('Step1', () => { it('should show a successful test', async () => { jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch: (arg: any) => { - if (arg.pathname === '/api/action/1/_execute') { - return { status: 'ok' }; - } - return {}; + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: (arg: any) => { + if (arg.pathname === '/api/action/1/_execute') { + return { status: 'ok' }; + } + return {}; + }, + }, }, })); setModules(); @@ -257,12 +277,16 @@ describe('Step1', () => { it('should show a failed test error', async () => { jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch: (arg: any) => { - if (arg.pathname === '/api/action/1/_execute') { - return { message: 'Very detailed error message' }; - } - return {}; + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: (arg: any) => { + if (arg.pathname === '/api/action/1/_execute') { + return { message: 'Very detailed error message' }; + } + return {}; + }, + }, }, })); setModules(); @@ -304,8 +328,12 @@ describe('Step1', () => { it('should send up the delete to the server', async () => { const kfetch = jest.fn().mockImplementation(() => {}); jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch, + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch, + }, + }, })); setModules(); }); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx index a69bf29dd9874..7953010005885 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx @@ -16,10 +16,10 @@ import { EuiToolTip, EuiCallOut, } from '@elastic/eui'; -import { kfetch } from 'ui/kfetch'; import { omit, pick } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ActionResult, BASE_ACTION_API_PATH } from '../../../../../../../plugins/actions/common'; +import { Legacy } from '../../../legacy_shims'; +import { ActionResult, BASE_ACTION_API_PATH } from '../../../../../../plugins/actions/common'; import { ManageEmailAction, EmailActionData } from '../manage_email_action'; import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; import { NEW_ACTION_ID } from './configuration'; @@ -42,7 +42,7 @@ export const Step1: React.FC = (props: GetStep1Props) => { async function createEmailAction(data: EmailActionData) { if (props.editAction) { - await kfetch({ + await Legacy.shims.kfetch({ method: 'PUT', pathname: `${BASE_ACTION_API_PATH}/${props.editAction.id}`, body: JSON.stringify({ @@ -53,7 +53,7 @@ export const Step1: React.FC = (props: GetStep1Props) => { }); props.setEditAction(null); } else { - await kfetch({ + await Legacy.shims.kfetch({ method: 'POST', pathname: BASE_ACTION_API_PATH, body: JSON.stringify({ @@ -73,7 +73,7 @@ export const Step1: React.FC = (props: GetStep1Props) => { async function deleteEmailAction(id: string) { setIsDeleting(true); - await kfetch({ + await Legacy.shims.kfetch({ method: 'DELETE', pathname: `${BASE_ACTION_API_PATH}/${id}`, }); @@ -99,7 +99,7 @@ export const Step1: React.FC = (props: GetStep1Props) => { to: [props.emailAddress], }; - const result = await kfetch({ + const result = await Legacy.shims.kfetch({ method: 'POST', pathname: `${BASE_ACTION_API_PATH}/${props.selectedEmailActionId}/_execute`, body: JSON.stringify({ params }), diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.tsx similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/step2.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.tsx similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/step3.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/formatted_alert.js b/x-pack/plugins/monitoring/public/components/alerts/formatted_alert.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/formatted_alert.js rename to x-pack/plugins/monitoring/public/components/alerts/formatted_alert.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/index.js b/x-pack/plugins/monitoring/public/components/alerts/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/index.js rename to x-pack/plugins/monitoring/public/components/alerts/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx b/x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx rename to x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx index 2bd9804795cb5..3ef9654076340 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx @@ -21,7 +21,7 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ActionResult } from '../../../../../../plugins/actions/common'; +import { ActionResult } from '../../../../../plugins/actions/common'; import { getMissingFieldErrors, hasErrors, getRequiredFieldError } from '../../lib/form_validation'; import { ALERT_EMAIL_SERVICES } from '../../../common/constants'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/map_severity.js b/x-pack/plugins/monitoring/public/components/alerts/map_severity.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/map_severity.js rename to x-pack/plugins/monitoring/public/components/alerts/map_severity.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/status.test.tsx similarity index 83% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx rename to x-pack/plugins/monitoring/public/components/alerts/status.test.tsx index d3cf4b463a2cc..a0031f50951bd 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/status.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { kfetch } from 'ui/kfetch'; +import { Legacy } from '../../legacy_shims'; import { AlertsStatus, AlertsStatusProps } from './status'; import { ALERT_TYPES } from '../../../common/constants'; import { getSetupModeState } from '../../lib/setup_mode'; @@ -18,8 +18,16 @@ jest.mock('../../lib/setup_mode', () => ({ toggleSetupMode: jest.fn(), })); -jest.mock('ui/kfetch', () => ({ - kfetch: jest.fn(), +jest.mock('../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: jest.fn(), + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'current', + }, + }, + }, })); const defaultProps: AlertsStatusProps = { @@ -35,7 +43,7 @@ describe('Status', () => { enabled: false, }); - (kfetch as jest.Mock).mockImplementation(({ pathname }) => { + (Legacy.shims.kfetch as jest.Mock).mockImplementation(({ pathname }) => { if (pathname === '/internal/security/api_key/privileges') { return { areApiKeysEnabled: true }; } @@ -62,7 +70,7 @@ describe('Status', () => { }); it('should render a success message if all alerts have been migrated and in setup mode', async () => { - (kfetch as jest.Mock).mockReturnValue({ + (Legacy.shims.kfetch as jest.Mock).mockReturnValue({ data: ALERT_TYPES.map(type => ({ alertTypeId: type })), }); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx b/x-pack/plugins/monitoring/public/components/alerts/status.tsx similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx rename to x-pack/plugins/monitoring/public/components/alerts/status.tsx index 5f5329bf7fff8..cdddbf1031303 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/status.tsx @@ -5,7 +5,6 @@ */ import React, { Fragment } from 'react'; -import { kfetch } from 'ui/kfetch'; import { EuiSpacer, EuiCallOut, @@ -18,8 +17,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { Alert, BASE_ALERT_API_PATH } from '../../../../../../plugins/alerting/common'; +import { Legacy } from '../../legacy_shims'; +import { Alert, BASE_ALERT_API_PATH } from '../../../../../plugins/alerting/common'; import { getSetupModeState, addSetupModeCallback, toggleSetupMode } from '../../lib/setup_mode'; import { NUMBER_OF_MIGRATED_ALERTS, ALERT_TYPE_PREFIX } from '../../../common/constants'; import { AlertsConfiguration } from './configuration'; @@ -39,7 +38,10 @@ export const AlertsStatus: React.FC = (props: AlertsStatusPro React.useEffect(() => { async function fetchAlertsStatus() { - const alerts = await kfetch({ method: 'GET', pathname: `${BASE_ALERT_API_PATH}/_find` }); + const alerts = await Legacy.shims.kfetch({ + method: 'GET', + pathname: `${BASE_ALERT_API_PATH}/_find`, + }); const monitoringAlerts = alerts.data.filter((alert: Alert) => alert.alertTypeId.startsWith(ALERT_TYPE_PREFIX) ); @@ -57,7 +59,9 @@ export const AlertsStatus: React.FC = (props: AlertsStatusPro }, [setupModeEnabled, showMigrationFlyout]); async function fetchSecurityConfigured() { - const response = await kfetch({ pathname: '/internal/security/api_key/privileges' }); + const response = await Legacy.shims.kfetch({ + pathname: '/internal/security/api_key/privileges', + }); setIsSecurityConfigured(response.areApiKeysEnabled); } @@ -72,7 +76,7 @@ export const AlertsStatus: React.FC = (props: AlertsStatusPro if (isSecurityConfigured) { return null; } - + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; const link = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/security-settings.html#api-key-service-settings`; return ( diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instance/index.js b/x-pack/plugins/monitoring/public/components/apm/instance/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/instance/index.js rename to x-pack/plugins/monitoring/public/components/apm/instance/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instance/instance.js b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/instance/instance.js rename to x-pack/plugins/monitoring/public/components/apm/instance/instance.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instance/status.js b/x-pack/plugins/monitoring/public/components/apm/instance/status.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/instance/status.js rename to x-pack/plugins/monitoring/public/components/apm/instance/status.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instances/index.js b/x-pack/plugins/monitoring/public/components/apm/instances/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/instances/index.js rename to x-pack/plugins/monitoring/public/components/apm/instances/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js rename to x-pack/plugins/monitoring/public/components/apm/instances/instances.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instances/status.js b/x-pack/plugins/monitoring/public/components/apm/instances/status.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/instances/status.js rename to x-pack/plugins/monitoring/public/components/apm/instances/status.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/overview/index.js b/x-pack/plugins/monitoring/public/components/apm/overview/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/overview/index.js rename to x-pack/plugins/monitoring/public/components/apm/overview/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/status_icon.js b/x-pack/plugins/monitoring/public/components/apm/status_icon.js similarity index 91% rename from x-pack/legacy/plugins/monitoring/public/components/apm/status_icon.js rename to x-pack/plugins/monitoring/public/components/apm/status_icon.js index 2de77b70df646..073c56217e8df 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/apm/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/apm/status_icon.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { StatusIcon } from 'plugins/monitoring/components/status_icon'; +import { StatusIcon } from '../../components/status_icon'; import { i18n } from '@kbn/i18n'; export function ApmStatusIcon({ status, availability = true }) { diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/beat/beat.js b/x-pack/plugins/monitoring/public/components/beats/beat/beat.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/beat/beat.js rename to x-pack/plugins/monitoring/public/components/beats/beat/beat.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/beat/index.js b/x-pack/plugins/monitoring/public/components/beats/beat/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/beat/index.js rename to x-pack/plugins/monitoring/public/components/beats/beat/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/index.js b/x-pack/plugins/monitoring/public/components/beats/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/index.js rename to x-pack/plugins/monitoring/public/components/beats/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/listing/index.js b/x-pack/plugins/monitoring/public/components/beats/listing/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/listing/index.js rename to x-pack/plugins/monitoring/public/components/beats/listing/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/listing/listing.js b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js similarity index 96% rename from x-pack/legacy/plugins/monitoring/public/components/beats/listing/listing.js rename to x-pack/plugins/monitoring/public/components/beats/listing/listing.js index a20728eb9a58f..5863f6e5161ad 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/beats/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js @@ -14,9 +14,9 @@ import { EuiLink, EuiScreenReaderOnly, } from '@elastic/eui'; -import { Stats } from 'plugins/monitoring/components/beats'; -import { formatMetric } from 'plugins/monitoring/lib/format_number'; -import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; +import { Stats } from '../../beats'; +import { formatMetric } from '../../../lib/format_number'; +import { EuiMonitoringTable } from '../../table'; import { i18n } from '@kbn/i18n'; import { BEATS_SYSTEM_ID } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_active.test.js.snap b/x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_active.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_active.test.js.snap rename to x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_active.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_types.test.js.snap b/x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_types.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_types.test.js.snap rename to x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_types.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_versions.test.js.snap b/x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_versions.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_versions.test.js.snap rename to x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_versions.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap b/x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap rename to x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/index.js b/x-pack/plugins/monitoring/public/components/beats/overview/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/index.js rename to x-pack/plugins/monitoring/public/components/beats/overview/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_active.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_active.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_active.js rename to x-pack/plugins/monitoring/public/components/beats/overview/latest_active.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_active.test.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_active.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_active.test.js rename to x-pack/plugins/monitoring/public/components/beats/overview/latest_active.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_types.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_types.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_types.js rename to x-pack/plugins/monitoring/public/components/beats/overview/latest_types.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_types.test.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_types.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_types.test.js rename to x-pack/plugins/monitoring/public/components/beats/overview/latest_types.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_versions.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_versions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_versions.js rename to x-pack/plugins/monitoring/public/components/beats/overview/latest_versions.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_versions.test.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_versions.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_versions.test.js rename to x-pack/plugins/monitoring/public/components/beats/overview/latest_versions.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/overview.js b/x-pack/plugins/monitoring/public/components/beats/overview/overview.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/overview.js rename to x-pack/plugins/monitoring/public/components/beats/overview/overview.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/overview.test.js b/x-pack/plugins/monitoring/public/components/beats/overview/overview.test.js similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/overview.test.js rename to x-pack/plugins/monitoring/public/components/beats/overview/overview.test.js index 1947f042b09b7..006f6ce3db975 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/overview.test.js +++ b/x-pack/plugins/monitoring/public/components/beats/overview/overview.test.js @@ -10,16 +10,10 @@ import { shallow } from 'enzyme'; jest.mock('../stats', () => ({ Stats: () => 'Stats', })); -jest.mock('../../', () => ({ +jest.mock('../../chart', () => ({ MonitoringTimeseriesContainer: () => 'MonitoringTimeseriesContainer', })); -jest.mock('../../../np_imports/ui/chrome', () => { - return { - getBasePath: () => '', - }; -}); - import { BeatsOverview } from './overview'; describe('Overview', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/stats.js b/x-pack/plugins/monitoring/public/components/beats/stats.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/beats/stats.js rename to x-pack/plugins/monitoring/public/components/beats/stats.js index 672d8a79ca64a..89ec10bbaf1bb 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/beats/stats.js +++ b/x-pack/plugins/monitoring/public/components/beats/stats.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { formatMetric } from 'plugins/monitoring/lib/format_number'; +import { formatMetric } from '../../lib/format_number'; import { SummaryStatus } from '../summary_status'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_color.js b/x-pack/plugins/monitoring/public/components/chart/__tests__/get_color.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_color.js rename to x-pack/plugins/monitoring/public/components/chart/__tests__/get_color.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_last_value.js b/x-pack/plugins/monitoring/public/components/chart/__tests__/get_last_value.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_last_value.js rename to x-pack/plugins/monitoring/public/components/chart/__tests__/get_last_value.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_title.js b/x-pack/plugins/monitoring/public/components/chart/__tests__/get_title.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_title.js rename to x-pack/plugins/monitoring/public/components/chart/__tests__/get_title.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_values_for_legend.js b/x-pack/plugins/monitoring/public/components/chart/__tests__/get_values_for_legend.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_values_for_legend.js rename to x-pack/plugins/monitoring/public/components/chart/__tests__/get_values_for_legend.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/_chart.scss b/x-pack/plugins/monitoring/public/components/chart/_chart.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/_chart.scss rename to x-pack/plugins/monitoring/public/components/chart/_chart.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/_index.scss b/x-pack/plugins/monitoring/public/components/chart/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/_index.scss rename to x-pack/plugins/monitoring/public/components/chart/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js b/x-pack/plugins/monitoring/public/components/chart/chart_target.js similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js rename to x-pack/plugins/monitoring/public/components/chart/chart_target.js index 5c9cddf9c2902..e6a6cc4b77755 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js +++ b/x-pack/plugins/monitoring/public/components/chart/chart_target.js @@ -6,7 +6,7 @@ import _ from 'lodash'; import React from 'react'; -import $ from 'plugins/xpack_main/jquery_flot'; +import $ from '../../lib/jquery_flot'; import { eventBus } from './event_bus'; import { getChartOptions } from './get_chart_options'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/event_bus.js b/x-pack/plugins/monitoring/public/components/chart/event_bus.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/event_bus.js rename to x-pack/plugins/monitoring/public/components/chart/event_bus.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js b/x-pack/plugins/monitoring/public/components/chart/get_chart_options.js similarity index 92% rename from x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js rename to x-pack/plugins/monitoring/public/components/chart/get_chart_options.js index 661d51e068201..81a3260447600 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js +++ b/x-pack/plugins/monitoring/public/components/chart/get_chart_options.js @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from '../../np_imports/ui/chrome'; +import { Legacy } from '../../legacy_shims'; import { merge } from 'lodash'; import { CHART_LINE_COLOR, CHART_TEXT_COLOR } from '../../../common/constants'; export async function getChartOptions(axisOptions) { - const $injector = chrome.dangerouslyGetActiveInjector(); + const $injector = Legacy.shims.getAngularInjector(); const timezone = $injector.get('config').get('dateFormat:tz'); const opts = { legend: { diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_color.js b/x-pack/plugins/monitoring/public/components/chart/get_color.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/get_color.js rename to x-pack/plugins/monitoring/public/components/chart/get_color.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_last_value.js b/x-pack/plugins/monitoring/public/components/chart/get_last_value.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/get_last_value.js rename to x-pack/plugins/monitoring/public/components/chart/get_last_value.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_title.js b/x-pack/plugins/monitoring/public/components/chart/get_title.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/get_title.js rename to x-pack/plugins/monitoring/public/components/chart/get_title.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_units.js b/x-pack/plugins/monitoring/public/components/chart/get_units.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/get_units.js rename to x-pack/plugins/monitoring/public/components/chart/get_units.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_values_for_legend.js b/x-pack/plugins/monitoring/public/components/chart/get_values_for_legend.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/get_values_for_legend.js rename to x-pack/plugins/monitoring/public/components/chart/get_values_for_legend.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/horizontal_legend.js b/x-pack/plugins/monitoring/public/components/chart/horizontal_legend.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/horizontal_legend.js rename to x-pack/plugins/monitoring/public/components/chart/horizontal_legend.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/index.js b/x-pack/plugins/monitoring/public/components/chart/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/index.js rename to x-pack/plugins/monitoring/public/components/chart/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/info_tooltip.js b/x-pack/plugins/monitoring/public/components/chart/info_tooltip.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/info_tooltip.js rename to x-pack/plugins/monitoring/public/components/chart/info_tooltip.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/monitoring_timeseries.js b/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/monitoring_timeseries.js rename to x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js b/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js rename to x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/timeseries_container.js b/x-pack/plugins/monitoring/public/components/chart/timeseries_container.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/timeseries_container.js rename to x-pack/plugins/monitoring/public/components/chart/timeseries_container.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/timeseries_visualization.js b/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/timeseries_visualization.js rename to x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js b/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js rename to x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js index 948f743ba0183..68d7a5a94e42f 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; +import { mapSeverity } from '../../alerts/map_severity'; import { EuiHealth, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/listing/index.js b/x-pack/plugins/monitoring/public/components/cluster/listing/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/listing/index.js rename to x-pack/plugins/monitoring/public/components/cluster/listing/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/listing/listing.js b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/listing/listing.js rename to x-pack/plugins/monitoring/public/components/cluster/listing/listing.js index 4cf74b3595730..feda891c1ce29 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment, Component } from 'react'; -import chrome from 'plugins/monitoring/np_imports/ui/chrome'; +import { Legacy } from '../../../legacy_shims'; import moment from 'moment'; import numeral from '@elastic/numeral'; import { capitalize, partial } from 'lodash'; @@ -19,12 +19,11 @@ import { EuiSpacer, EuiIcon, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; -import { AlertsIndicator } from 'plugins/monitoring/components/cluster/listing/alerts_indicator'; +import { EuiMonitoringTable } from '../../table'; +import { AlertsIndicator } from '../../cluster/listing/alerts_indicator'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; const IsClusterSupported = ({ isSupported, children }) => { @@ -236,13 +235,17 @@ const changeCluster = (scope, globalState, kbnUrl, clusterUuid, ccs) => { globalState.cluster_uuid = clusterUuid; globalState.ccs = ccs; globalState.save(); - kbnUrl.changePath('/overview'); + kbnUrl.redirect('/overview'); }); }; const licenseWarning = (scope, { title, text }) => { scope.$evalAsync(() => { - toastNotifications.addWarning({ title, text, 'data-test-subj': 'monitoringLicenseWarning' }); + Legacy.shims.toastNotifications.addWarning({ + title, + text, + 'data-test-subj': 'monitoringLicenseWarning', + }); }); }; @@ -285,7 +288,7 @@ const handleClickIncompatibleLicense = (scope, clusterName) => { }; const handleClickInvalidLicense = (scope, clusterName) => { - const licensingPath = `${chrome.getBasePath()}/app/kibana#/management/elasticsearch/license_management/home`; + const licensingPath = `${Legacy.shims.getBasePath()}/app/kibana#/management/elasticsearch/license_management/home`; licenseWarning(scope, { title: toMountPoint( diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/__tests__/__snapshots__/helpers.test.js.snap b/x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/__snapshots__/helpers.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/__tests__/__snapshots__/helpers.test.js.snap rename to x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/__snapshots__/helpers.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js b/x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js index fea8f0001540a..e09c42bc59429 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { BytesUsage, BytesPercentageUsage } from '../helpers'; describe('Bytes Usage', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js index d87ff98e79be0..6dcd64f875e1c 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js @@ -6,8 +6,8 @@ import React, { Fragment } from 'react'; import moment from 'moment-timezone'; -import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; -import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; +import { FormattedAlert } from '../../alerts/formatted_alert'; +import { mapSeverity } from '../../alerts/map_severity'; import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; import { CALCULATE_DURATION_SINCE, diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/apm_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/apm_panel.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js index 84dc13e9da1de..97205e0fcd732 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/apm_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js @@ -7,7 +7,7 @@ import React from 'react'; import moment from 'moment'; import { get } from 'lodash'; -import { formatMetric } from 'plugins/monitoring/lib/format_number'; +import { formatMetric } from '../../../lib/format_number'; import { ClusterItemContainer, BytesUsage, DisabledIfNoDataAndInSetupModeLink } from './helpers'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/beats_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/beats_panel.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js index 7406c15f3cf1d..c20770bdda6b7 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/beats_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js @@ -6,7 +6,7 @@ import { get } from 'lodash'; import React from 'react'; -import { formatMetric } from 'plugins/monitoring/lib/format_number'; +import { formatMetric } from '../../../lib/format_number'; import { EuiFlexGrid, EuiFlexItem, diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index 48ff8edaafe60..41dc662c94211 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { get, capitalize } from 'lodash'; -import { formatNumber } from 'plugins/monitoring/lib/format_number'; +import { formatNumber } from '../../../lib/format_number'; import { ClusterItemContainer, HealthStatusIndicator, diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/helpers.js b/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/helpers.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/kibana_panel.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index ee17ce446dd6d..541c240b3c35a 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { formatNumber } from 'plugins/monitoring/lib/format_number'; +import { formatNumber } from '../../../lib/format_number'; import { ClusterItemContainer, HealthStatusIndicator, diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/license_text.js b/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/license_text.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/logstash_panel.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index 9e21ef1074ed3..19a318642aab7 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { formatNumber } from 'plugins/monitoring/lib/format_number'; +import { formatNumber } from '../../../lib/format_number'; import { ClusterItemContainer, BytesPercentageUsage, diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/status_icon.js b/x-pack/plugins/monitoring/public/components/cluster/status_icon.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/status_icon.js rename to x-pack/plugins/monitoring/public/components/cluster/status_icon.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/__snapshots__/ccr.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/__snapshots__/ccr.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/__snapshots__/ccr.test.js.snap rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr/__snapshots__/ccr.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js index cddae16f34eca..5aad73d7a131e 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js @@ -17,10 +17,9 @@ import { EuiTextColor, EuiScreenReaderOnly, } from '@elastic/eui'; -import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; -import './ccr.css'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; function toSeconds(ms) { return Math.floor(ms / 1000) + 's'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.css b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.css rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr/index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js index af0ff323b7ba8..a8aa931bad254 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js @@ -5,7 +5,7 @@ */ import React, { Fragment, PureComponent } from 'react'; -import chrome from '../../../np_imports/ui/chrome'; +import { Legacy } from '../../../legacy_shims'; import { EuiPage, EuiPageBody, @@ -93,7 +93,7 @@ export class CcrShard extends PureComponent { renderLatestStat() { const { stat, timestamp } = this.props; - const injector = chrome.dangerouslyGetActiveInjector(); + const injector = Legacy.shims.getAngularInjector(); const timezone = injector.get('config').get('dateFormat:tz'); return ( diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js similarity index 88% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js index b950c2ca0a6d2..ceb1fd1186eed 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js @@ -8,13 +8,18 @@ import React from 'react'; import { shallow } from 'enzyme'; import { CcrShard } from './ccr_shard'; -jest.mock('../../../np_imports/ui/chrome', () => { +jest.mock('../../../legacy_shims', () => { return { - getBasePath: () => '', - dangerouslyGetActiveInjector: () => ({ get: () => ({ get: () => 'utc' }) }), + Legacy: { + shims: { getAngularInjector: () => ({ get: () => ({ get: () => 'utc' }) }) }, + }, }; }); +jest.mock('../../chart', () => ({ + MonitoringTimeseriesContainer: () => 'MonitoringTimeseriesContainer', +})); + describe('CcrShard', () => { const props = { formattedLeader: 'leader on remote', diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index/advanced.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/advanced.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index/advanced.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/index/advanced.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/indices/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js index c2775713171ad..4d457c943c872 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { StatusIcon } from 'plugins/monitoring/components/status_icon'; +import { StatusIcon } from '../../status_icon'; import { i18n } from '@kbn/i18n'; export function MachineLearningJobStatusIcon({ status }) { diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/advanced.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/advanced.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/node/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/node.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/node.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/status_icon.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap rename to x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js index 50abdcc718bc8..0c4b4b2b3c3f4 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { MetricCell } from '../cells'; describe('Node Listing Metric Cell', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/cells.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/cells.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/nodes/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/overview/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/overview/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/overview/overview.js b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/overview/overview.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js similarity index 92% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js index 133b520947b1b..1aaba96ed3da4 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from '../../../np_imports/ui/chrome'; +import { Legacy } from '../../../legacy_shims'; import { capitalize } from 'lodash'; -import { formatMetric } from 'plugins/monitoring/lib/format_number'; +import { formatMetric } from '../../../lib/format_number'; import { formatDateTimeLocal } from '../../../../common/formatting'; const getIpAndPort = transport => { @@ -39,7 +39,7 @@ export const parseProps = props => { } = props; const { files, size } = index; - const injector = chrome.dangerouslyGetActiveInjector(); + const injector = Legacy.shims.getAngularInjector(); const timezone = injector.get('config').get('dateFormat:tz'); return { diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js index 6607d236590c1..d228144778c02 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { EuiText, EuiTitle, EuiLink, EuiSpacer, EuiSwitch } from '@elastic/eui'; -import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; +import { EuiMonitoringTable } from '../../table'; import { RecoveryIndex } from './recovery_index'; import { TotalTime } from './total_time'; import { SourceDestination } from './source_destination'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/source_destination.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/source_destination.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/source_destination.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/source_destination.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/_index.scss b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/_index.scss rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/__snapshots__/shard.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/__snapshots__/shard.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/__snapshots__/shard.test.js.snap rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/__snapshots__/shard.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.test.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/calculate_class.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/calculate_class.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/calculate_class.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/calculate_class.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.test.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/generate_query_and_link.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/generate_query_and_link.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/generate_query_and_link.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/generate_query_and_link.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_unassigned.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_unassigned.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_unassigned.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_unassigned.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/labels.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/labels.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/labels.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/labels.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/status_icon.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/index.js b/x-pack/plugins/monitoring/public/components/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/index.js rename to x-pack/plugins/monitoring/public/components/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/kibana/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/kibana/cluster_status/index.js rename to x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/kibana/detail_status/index.js b/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/kibana/detail_status/index.js rename to x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/kibana/instances/index.js b/x-pack/plugins/monitoring/public/components/kibana/instances/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/kibana/instances/index.js rename to x-pack/plugins/monitoring/public/components/kibana/instances/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/kibana/instances/instances.js b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/components/kibana/instances/instances.js rename to x-pack/plugins/monitoring/public/components/kibana/instances/instances.js index 3a30bbc38d426..ed562c7da995b 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/kibana/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js @@ -19,7 +19,7 @@ import { capitalize, get } from 'lodash'; import { ClusterStatus } from '../cluster_status'; import { EuiMonitoringTable } from '../../table'; import { KibanaStatusIcon } from '../status_icon'; -import { StatusIcon } from 'plugins/monitoring/components/status_icon'; +import { StatusIcon } from '../../status_icon'; import { formatMetric, formatNumber } from '../../../lib/format_number'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/kibana/status_icon.js b/x-pack/plugins/monitoring/public/components/kibana/status_icon.js similarity index 91% rename from x-pack/legacy/plugins/monitoring/public/components/kibana/status_icon.js rename to x-pack/plugins/monitoring/public/components/kibana/status_icon.js index 87a2ab4cc4713..3c18197170b50 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/kibana/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/kibana/status_icon.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { StatusIcon } from 'plugins/monitoring/components/status_icon'; +import { StatusIcon } from '../status_icon'; import { i18n } from '@kbn/i18n'; export function KibanaStatusIcon({ status, availability = true }) { diff --git a/x-pack/plugins/monitoring/public/components/license/index.js b/x-pack/plugins/monitoring/public/components/license/index.js new file mode 100644 index 0000000000000..085cc9082cf53 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/license/index.js @@ -0,0 +1,201 @@ +/* + * 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, { Fragment } from 'react'; +import { + EuiPage, + EuiPageBody, + EuiSpacer, + EuiCodeBlock, + EuiPanel, + EuiText, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiScreenReaderOnly, + EuiCard, + EuiButton, + EuiIcon, + EuiTitle, + EuiTextAlign, +} from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { Legacy } from '../../legacy_shims'; + +export const AddLicense = ({ uploadPath }) => { + return ( + + } + description={ + + } + footer={ + + + + } + /> + ); +}; + +export class LicenseStatus extends React.PureComponent { + render() { + const { isExpired, status, type, expiryDate } = this.props; + const typeTitleCase = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase(); + let icon; + let title; + let message; + if (isExpired) { + icon = ; + message = ( + + {expiryDate}, + }} + /> + + ); + title = ( + + ); + } else { + icon = ; + message = expiryDate ? ( + + {expiryDate}, + }} + /> + + ) : ( + + + + ); + title = ( + + ); + } + return ( + + + {icon} + + +

{title}

+
+
+
+ + + + {message} +
+ ); + } +} + +const LicenseUpdateInfoForPrimary = ({ isPrimaryCluster, uploadLicensePath }) => { + if (!isPrimaryCluster) { + return null; + } + + // viewed license is for the cluster directly connected to Kibana + return ; +}; + +const LicenseUpdateInfoForRemote = ({ isPrimaryCluster }) => { + if (isPrimaryCluster) { + return null; + } + + // viewed license is for a remote monitored cluster not directly connected to Kibana + return ( + +

+ +

+ + + {`curl -XPUT -u 'https://:/_license' -H 'Content-Type: application/json' -d @license.json`} + +
+ ); +}; + +export function License(props) { + const { status, type, isExpired, expiryDate } = props; + const licenseManagement = `${Legacy.shims.getBasePath()}/app/kibana#/management/elasticsearch/license_management`; + return ( + + +

+ +

+
+ + + + + + + + + + + + + +

+ For more license options please visit  + License Management. +

+
+
+
+ ); +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/__snapshots__/logs.test.js.snap b/x-pack/plugins/monitoring/public/components/logs/__snapshots__/logs.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logs/__snapshots__/logs.test.js.snap rename to x-pack/plugins/monitoring/public/components/logs/__snapshots__/logs.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap b/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap rename to x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/index.js b/x-pack/plugins/monitoring/public/components/logs/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logs/index.js rename to x-pack/plugins/monitoring/public/components/logs/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js b/x-pack/plugins/monitoring/public/components/logs/logs.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/logs/logs.js rename to x-pack/plugins/monitoring/public/components/logs/logs.js index 3590199048352..eaf92f72103e0 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js +++ b/x-pack/plugins/monitoring/public/components/logs/logs.js @@ -5,17 +5,16 @@ */ import React, { PureComponent } from 'react'; import { capitalize } from 'lodash'; -import chrome from '../../np_imports/ui/chrome'; +import { Legacy } from '../../legacy_shims'; import { EuiBasicTable, EuiTitle, EuiSpacer, EuiText, EuiCallOut, EuiLink } from '@elastic/eui'; import { INFRA_SOURCE_ID } from '../../../common/constants'; import { formatDateTimeLocal } from '../../../common/formatting'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Reason } from './reason'; -import { capabilities } from '../../np_imports/ui/capabilities'; const getFormattedDateTimeLocal = timestamp => { - const injector = chrome.dangerouslyGetActiveInjector(); + const injector = Legacy.shims.getAngularInjector(); const timezone = injector.get('config').get('dateFormat:tz'); return formatDateTimeLocal(timestamp, timezone); }; @@ -110,7 +109,7 @@ const clusterColumns = [ ]; function getLogsUiLink(clusterUuid, nodeId, indexUuid) { - const base = `${chrome.getBasePath()}/app/logs/link-to/${INFRA_SOURCE_ID}/logs`; + const base = `${Legacy.shims.getBasePath()}/app/logs/link-to/${INFRA_SOURCE_ID}/logs`; const params = []; if (clusterUuid) { @@ -158,7 +157,7 @@ export class Logs extends PureComponent { } renderCallout() { - const uiCapabilities = capabilities.get(); + const uiCapabilities = Legacy.shims.capabilities.get(); const show = uiCapabilities.logs && uiCapabilities.logs.show; const { logs: { enabled }, diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.test.js b/x-pack/plugins/monitoring/public/components/logs/logs.test.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/logs/logs.test.js rename to x-pack/plugins/monitoring/public/components/logs/logs.test.js index 63af8b208fbec..c0497ee351e34 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.test.js +++ b/x-pack/plugins/monitoring/public/components/logs/logs.test.js @@ -8,21 +8,16 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Logs } from './logs'; -jest.mock('../../np_imports/ui/chrome', () => { - return { - getBasePath: () => '', - }; -}); - -jest.mock( - '../../np_imports/ui/capabilities', - () => ({ - capabilities: { - get: () => ({ logs: { show: true } }), +jest.mock('../../legacy_shims', () => ({ + Legacy: { + shims: { + getBasePath: () => '', + capabilities: { + get: () => ({ logs: { show: true } }), + }, }, - }), - { virtual: true } -); + }, +})); const logs = { enabled: true, diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/reason.js b/x-pack/plugins/monitoring/public/components/logs/reason.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/logs/reason.js rename to x-pack/plugins/monitoring/public/components/logs/reason.js index 91c62ae53a1ca..ad21f7f81d9bd 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logs/reason.js +++ b/x-pack/plugins/monitoring/public/components/logs/reason.js @@ -8,10 +8,11 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Legacy } from '../../legacy_shims'; import { Monospace } from '../metricbeat_migration/instruction_steps/components/monospace/monospace'; export const Reason = ({ reason }) => { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; let title = i18n.translate('xpack.monitoring.logs.reason.defaultTitle', { defaultMessage: 'No log data found', }); diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/reason.test.js b/x-pack/plugins/monitoring/public/components/logs/reason.test.js similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/logs/reason.test.js rename to x-pack/plugins/monitoring/public/components/logs/reason.test.js index c8ed05bd73ade..bd56c733268b7 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logs/reason.test.js +++ b/x-pack/plugins/monitoring/public/components/logs/reason.test.js @@ -8,9 +8,15 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Reason } from './reason'; -jest.mock('ui/documentation_links', () => ({ - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - DOC_LINK_VERSION: 'current', +jest.mock('../../legacy_shims', () => ({ + Legacy: { + shims: { + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'current', + }, + }, + }, })); describe('Logs', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/cluster_status/index.js rename to x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/detail_status/index.js b/x-pack/plugins/monitoring/public/components/logstash/detail_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/detail_status/index.js rename to x-pack/plugins/monitoring/public/components/logstash/detail_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/listing/index.js b/x-pack/plugins/monitoring/public/components/logstash/listing/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/listing/index.js rename to x-pack/plugins/monitoring/public/components/logstash/listing/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/listing/listing.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/listing/listing.js rename to x-pack/plugins/monitoring/public/components/logstash/listing/listing.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/listing/listing.test.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/listing/listing.test.js rename to x-pack/plugins/monitoring/public/components/logstash/listing/listing.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/overview/index.js b/x-pack/plugins/monitoring/public/components/logstash/overview/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/overview/index.js rename to x-pack/plugins/monitoring/public/components/logstash/overview/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/overview/overview.js b/x-pack/plugins/monitoring/public/components/logstash/overview/overview.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/overview/overview.js rename to x-pack/plugins/monitoring/public/components/logstash/overview/overview.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_listing/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_listing/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js index f8df93d6ee8fb..132cc7ce131cf 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { formatMetric } from '../../../lib/format_number'; import { ClusterStatus } from '../cluster_status'; -import { Sparkline } from 'plugins/monitoring/components/sparkline'; +import { Sparkline } from '../../../components/sparkline'; import { EuiMonitoringSSPTable } from '../../table'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/config.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/config.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/config.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/config.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/pipeline_state.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/pipeline_state.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/pipeline_state.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/pipeline_state.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/config.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/config.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/config.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/config.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/boolean_edge.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/boolean_edge.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/boolean_edge.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/boolean_edge.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge_factory.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge_factory.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge_factory.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge_factory.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/if_vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/if_vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/if_vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/if_vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/plugin_vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/plugin_vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/plugin_vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/plugin_vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/queue_vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/queue_vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/queue_vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/queue_vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex_factory.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex_factory.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex_factory.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex_factory.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/boolean_edge.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/boolean_edge.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/boolean_edge.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/boolean_edge.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge_factory.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge_factory.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge_factory.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge_factory.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/if_vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/if_vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/if_vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/if_vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/plugin_vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/plugin_vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/plugin_vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/plugin_vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/queue_vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/queue_vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/queue_vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/queue_vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex_factory.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex_factory.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex_factory.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex_factory.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/element.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/element.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/element.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/element.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/if_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/if_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/if_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/if_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/make_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/make_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/make_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/make_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/pipeline.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/pipeline.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/pipeline.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/pipeline.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/plugin_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/plugin_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/plugin_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/plugin_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/queue.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/queue.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/queue.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/queue.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/utils.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/utils.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/utils.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/utils.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/if_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/if_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/if_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/if_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/make_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/make_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/make_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/make_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/pipeline.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/pipeline.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/pipeline.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/pipeline.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/plugin_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/plugin_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/plugin_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/plugin_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/queue.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/queue.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/queue.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/queue.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/utils.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/utils.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/utils.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/utils.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline_state.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline_state.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline_state.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline_state.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/collapsible_statement.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/collapsible_statement.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/collapsible_statement.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/collapsible_statement.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/metric.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/metric.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/metric.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/metric.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/pipeline_viewer.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/pipeline_viewer.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/pipeline_viewer.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/pipeline_viewer.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/plugin_statement.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/plugin_statement.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/plugin_statement.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/plugin_statement.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/queue.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/queue.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/queue.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/queue.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list_heading.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list_heading.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list_heading.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list_heading.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_section.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_section.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_section.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_section.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/collapsible_statement.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/collapsible_statement.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/collapsible_statement.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/collapsible_statement.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js index 2a65110f81169..09f4d03953038 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js @@ -8,6 +8,10 @@ import React from 'react'; import { DetailDrawer } from '../detail_drawer'; import { shallow } from 'enzyme'; +jest.mock('../../../../sparkline', () => ({ + Sparkline: () => 'Sparkline', +})); + describe('DetailDrawer component', () => { let onHide; let timeseriesTooltipXValueFormatter; diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/metric.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/metric.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/metric.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/metric.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js similarity index 94% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js index 5013c38ac1921..8c2558bee4e44 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js @@ -8,6 +8,10 @@ import React from 'react'; import { PipelineViewer } from '../pipeline_viewer'; import { shallow } from 'enzyme'; +jest.mock('../../../../sparkline', () => ({ + Sparkline: () => 'Sparkline', +})); + describe('PipelineViewer component', () => { let pipeline; let component; diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/queue.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/queue.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/queue.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/queue.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list_heading.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list_heading.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list_heading.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list_heading.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_section.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_section.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_section.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_section.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_index.scss b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_index.scss rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_pipeline_viewer.scss b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_pipeline_viewer.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_pipeline_viewer.scss rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_pipeline_viewer.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/collapsible_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/collapsible_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/collapsible_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/collapsible_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/metric.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/metric.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/metric.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/metric.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/queue.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/queue.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/queue.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/queue.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list_heading.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list_heading.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list_heading.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list_heading.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_section.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_section.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_section.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_section.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/constants.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/constants.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/constants.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/constants.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js index 0a284c102dc9d..42d0eec7cbed0 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js @@ -25,7 +25,7 @@ import { EuiCheckbox, } from '@elastic/eui'; import { getInstructionSteps } from '../instruction_steps'; -import { Storage } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; import { STORAGE_KEY, ELASTICSEARCH_SYSTEM_ID, @@ -38,7 +38,7 @@ import { INSTRUCTION_STEP_ENABLE_METRICBEAT, INSTRUCTION_STEP_DISABLE_INTERNAL, } from '../constants'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Legacy } from '../../../legacy_shims'; import { getIdentifier, formatProductName } from '../../setup_mode/formatting'; const storage = new Storage(window.localStorage); @@ -223,6 +223,7 @@ export class Flyout extends Component { getDocumentationTitle() { const { productName } = this.props; + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; let documentationUrl = null; if (productName === KIBANA_SYSTEM_ID) { diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js index 3587381f977cb..61cfb491f0bd0 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js @@ -16,9 +16,16 @@ import { LOGSTASH_SYSTEM_ID, } from '../../../../common/constants'; -jest.mock('ui/documentation_links', () => ({ - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - DOC_LINK_VERSION: 'current', +jest.mock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: jest.fn(), + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'current', + }, + }, + }, })); jest.mock('../../../../common', () => ({ diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/disable_internal_collection_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/disable_internal_collection_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/disable_internal_collection_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/disable_internal_collection_instructions.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js index 9fa33802b0202..51a7a139aafd0 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js @@ -8,10 +8,11 @@ import React, { Fragment } from 'react'; import { EuiSpacer, EuiCodeBlock, EuiLink, EuiText } from '@elastic/eui'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Legacy } from '../../../../legacy_shims'; import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; export function getApmInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; const securitySetup = getSecurityStep( `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-configuration.html` ); diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js index 56e7c9b5af064..ddaa610b874a8 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js @@ -9,10 +9,11 @@ import { EuiSpacer, EuiCodeBlock, EuiLink, EuiCallOut, EuiText } from '@elastic/ import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; import { UNDETECTED_BEAT_TYPE, DEFAULT_BEAT_FOR_URLS } from './common_beats_instructions'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Legacy } from '../../../../legacy_shims'; import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; export function getBeatsInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; const beatType = product.beatType; const securitySetup = getSecurityStep( `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-configuration.html` diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/monospace.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/monospace.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/monospace.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/monospace.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js index 36b3dd21ff43e..8607739e7090c 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { EuiSpacer, EuiCodeBlock, EuiLink, EuiText } from '@elastic/eui'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Legacy } from '../../../../legacy_shims'; import { getSecurityStep, getMigrationStatusStep } from '../common_instructions'; export function getElasticsearchInstructionsForEnablingMetricbeat( @@ -16,6 +16,7 @@ export function getElasticsearchInstructionsForEnablingMetricbeat( _meta, { esMonitoringUrl } ) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; const securitySetup = getSecurityStep( `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html` ); diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js index 98c75dbfe4b37..eb1aedd47c568 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js @@ -8,10 +8,11 @@ import React, { Fragment } from 'react'; import { EuiSpacer, EuiCodeBlock, EuiLink, EuiText } from '@elastic/eui'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Legacy } from '../../../../legacy_shims'; import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; export function getKibanaInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; const securitySetup = getSecurityStep( `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html` ); diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js index 4a36f394e4bd5..ce542fa8a05e5 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js @@ -8,10 +8,11 @@ import React, { Fragment } from 'react'; import { EuiSpacer, EuiCodeBlock, EuiLink, EuiText } from '@elastic/eui'; import { Monospace } from '../components/monospace'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Legacy } from '../../../../legacy_shims'; import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; export function getLogstashInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; const securitySetup = getSecurityStep( `${ELASTIC_WEBSITE_URL}guide/en/logstash/${DOC_LINK_VERSION}/monitoring-with-metricbeat.html` ); diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/checker_errors.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/checker_errors.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/checker_errors.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/checker_errors.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js b/x-pack/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js similarity index 92% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js rename to x-pack/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js index 8462d2a6fc87b..f3cd5ccb9703d 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { boomify, forbidden } from 'boom'; -import { renderWithIntl } from '../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { CheckerErrors } from '../checker_errors'; describe('CheckerErrors', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js b/x-pack/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js similarity index 84% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js rename to x-pack/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js index 81a412a680bc6..5b54df3a82812 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js @@ -5,17 +5,11 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { NoData } from '../'; const enabler = {}; -jest.mock('../../../np_imports/ui/chrome', () => { - return { - getBasePath: () => '', - }; -}); - describe('NoData', () => { test('should show text next to the spinner while checking a setting', () => { const component = renderWithIntl( diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/_index.scss b/x-pack/plugins/monitoring/public/components/no_data/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/_index.scss rename to x-pack/plugins/monitoring/public/components/no_data/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/_no_data.scss b/x-pack/plugins/monitoring/public/components/no_data/_no_data.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/_no_data.scss rename to x-pack/plugins/monitoring/public/components/no_data/_no_data.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/changes_needed.js b/x-pack/plugins/monitoring/public/components/no_data/blurbs/changes_needed.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/changes_needed.js rename to x-pack/plugins/monitoring/public/components/no_data/blurbs/changes_needed.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/cloud_deployment.js b/x-pack/plugins/monitoring/public/components/no_data/blurbs/cloud_deployment.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/cloud_deployment.js rename to x-pack/plugins/monitoring/public/components/no_data/blurbs/cloud_deployment.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/index.js b/x-pack/plugins/monitoring/public/components/no_data/blurbs/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/index.js rename to x-pack/plugins/monitoring/public/components/no_data/blurbs/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/looking_for.js b/x-pack/plugins/monitoring/public/components/no_data/blurbs/looking_for.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/looking_for.js rename to x-pack/plugins/monitoring/public/components/no_data/blurbs/looking_for.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/what_is.js b/x-pack/plugins/monitoring/public/components/no_data/blurbs/what_is.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/what_is.js rename to x-pack/plugins/monitoring/public/components/no_data/blurbs/what_is.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/checker_errors.js b/x-pack/plugins/monitoring/public/components/no_data/checker_errors.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/checker_errors.js rename to x-pack/plugins/monitoring/public/components/no_data/checker_errors.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/checking_settings.js b/x-pack/plugins/monitoring/public/components/no_data/checking_settings.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/checking_settings.js rename to x-pack/plugins/monitoring/public/components/no_data/checking_settings.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js index d2be217ca8498..13a49b1c7b200 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js @@ -6,7 +6,7 @@ import React from 'react'; import sinon from 'sinon'; -import { mountWithIntl } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ExplainCollectionEnabled } from '../collection_enabled'; import { findTestSubject } from '@elastic/eui/lib/test'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js similarity index 96% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js index 0a50ad44c8b4f..88185283b6faa 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js @@ -6,7 +6,7 @@ import React from 'react'; import sinon from 'sinon'; -import { mountWithIntl } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ExplainCollectionInterval } from '../collection_interval'; import { findTestSubject } from '@elastic/eui/lib/test'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/__snapshots__/exporters.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/__snapshots__/exporters.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/__snapshots__/exporters.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/__snapshots__/exporters.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js index c9147037f0022..88f1eedfc3bcc 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { ExplainExporters, ExplainExportersCloud } from '../exporters'; // Mocking to prevent errors with React portal. diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/exporters.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/exporters.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/exporters.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/exporters.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/index.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/index.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/__snapshots__/plugin_enabled.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/__snapshots__/plugin_enabled.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/__snapshots__/plugin_enabled.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/__snapshots__/plugin_enabled.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js similarity index 92% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js index 56536a8e4270b..ece58ad6aeed2 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { ExplainPluginEnabled } from '../plugin_enabled'; // Mocking to prevent errors with React portal. diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/plugin_enabled.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/plugin_enabled.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/plugin_enabled.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/plugin_enabled.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/index.js b/x-pack/plugins/monitoring/public/components/no_data/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/index.js rename to x-pack/plugins/monitoring/public/components/no_data/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/no_data.js b/x-pack/plugins/monitoring/public/components/no_data/no_data.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/no_data.js rename to x-pack/plugins/monitoring/public/components/no_data/no_data.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/we_tried.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/we_tried.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/we_tried.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/we_tried.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js similarity index 96% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js rename to x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js index e9b2ff11538ab..7f15cacb1ebb9 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { ReasonFound } from '../'; // Mocking to prevent errors with React portal. diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js similarity index 85% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js rename to x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js index e382a1c9ea8db..95970453d4b7c 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { WeTried } from '../'; describe('WeTried', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/index.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/index.js rename to x-pack/plugins/monitoring/public/components/no_data/reasons/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/reason_found.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/reason_found.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/reason_found.js rename to x-pack/plugins/monitoring/public/components/no_data/reasons/reason_found.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/we_tried.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/we_tried.js rename to x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap b/x-pack/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap rename to x-pack/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js b/x-pack/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js similarity index 85% rename from x-pack/legacy/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js rename to x-pack/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js index b1ad00ffcc3c6..41f3ef4be969e 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js +++ b/x-pack/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { PageLoading } from '../'; describe('PageLoading', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/page_loading/index.js b/x-pack/plugins/monitoring/public/components/page_loading/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/page_loading/index.js rename to x-pack/plugins/monitoring/public/components/page_loading/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap b/x-pack/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap rename to x-pack/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/index.js b/x-pack/plugins/monitoring/public/components/renderers/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/renderers/index.js rename to x-pack/plugins/monitoring/public/components/renderers/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/lib/find_new_uuid.js b/x-pack/plugins/monitoring/public/components/renderers/lib/find_new_uuid.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/renderers/lib/find_new_uuid.js rename to x-pack/plugins/monitoring/public/components/renderers/lib/find_new_uuid.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js rename to x-pack/plugins/monitoring/public/components/renderers/setup_mode.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.test.js b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.test.js rename to x-pack/plugins/monitoring/public/components/renderers/setup_mode.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/badge.test.js.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/badge.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/badge.test.js.snap rename to x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/badge.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap rename to x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/formatting.test.js.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/formatting.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/formatting.test.js.snap rename to x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/formatting.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/listing_callout.test.js.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/listing_callout.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/listing_callout.test.js.snap rename to x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/listing_callout.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap rename to x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_enter_button.scss b/x-pack/plugins/monitoring/public/components/setup_mode/_enter_button.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/_enter_button.scss rename to x-pack/plugins/monitoring/public/components/setup_mode/_enter_button.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_index.scss b/x-pack/plugins/monitoring/public/components/setup_mode/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/_index.scss rename to x-pack/plugins/monitoring/public/components/setup_mode/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.js b/x-pack/plugins/monitoring/public/components/setup_mode/badge.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.js rename to x-pack/plugins/monitoring/public/components/setup_mode/badge.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.test.js b/x-pack/plugins/monitoring/public/components/setup_mode/badge.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.test.js rename to x-pack/plugins/monitoring/public/components/setup_mode/badge.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx b/x-pack/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx rename to x-pack/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.tsx b/x-pack/plugins/monitoring/public/components/setup_mode/enter_button.tsx similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.tsx rename to x-pack/plugins/monitoring/public/components/setup_mode/enter_button.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.js b/x-pack/plugins/monitoring/public/components/setup_mode/formatting.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.js rename to x-pack/plugins/monitoring/public/components/setup_mode/formatting.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.test.js b/x-pack/plugins/monitoring/public/components/setup_mode/formatting.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.test.js rename to x-pack/plugins/monitoring/public/components/setup_mode/formatting.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.js b/x-pack/plugins/monitoring/public/components/setup_mode/listing_callout.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.js rename to x-pack/plugins/monitoring/public/components/setup_mode/listing_callout.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.test.js b/x-pack/plugins/monitoring/public/components/setup_mode/listing_callout.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.test.js rename to x-pack/plugins/monitoring/public/components/setup_mode/listing_callout.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.js b/x-pack/plugins/monitoring/public/components/setup_mode/tooltip.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.js rename to x-pack/plugins/monitoring/public/components/setup_mode/tooltip.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.test.js b/x-pack/plugins/monitoring/public/components/setup_mode/tooltip.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.test.js rename to x-pack/plugins/monitoring/public/components/setup_mode/tooltip.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js b/x-pack/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js rename to x-pack/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/__test__/__snapshots__/index.test.js.snap b/x-pack/plugins/monitoring/public/components/sparkline/__test__/__snapshots__/index.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/__test__/__snapshots__/index.test.js.snap rename to x-pack/plugins/monitoring/public/components/sparkline/__test__/__snapshots__/index.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/__test__/index.test.js b/x-pack/plugins/monitoring/public/components/sparkline/__test__/index.test.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/__test__/index.test.js rename to x-pack/plugins/monitoring/public/components/sparkline/__test__/index.test.js index 22f4787593fec..ab2cf10b4615d 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/sparkline/__test__/index.test.js +++ b/x-pack/plugins/monitoring/public/components/sparkline/__test__/index.test.js @@ -9,6 +9,10 @@ import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; import { Sparkline } from '../'; +jest.mock('../sparkline_flot_chart', () => ({ + SparklineFlotChart: () => 'SparklineFlotChart', +})); + describe('Sparkline component', () => { let component; let renderedComponent; diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/_index.scss b/x-pack/plugins/monitoring/public/components/sparkline/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/_index.scss rename to x-pack/plugins/monitoring/public/components/sparkline/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/_sparkline.scss b/x-pack/plugins/monitoring/public/components/sparkline/_sparkline.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/_sparkline.scss rename to x-pack/plugins/monitoring/public/components/sparkline/_sparkline.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/index.js b/x-pack/plugins/monitoring/public/components/sparkline/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/index.js rename to x-pack/plugins/monitoring/public/components/sparkline/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js b/x-pack/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js rename to x-pack/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js index bb17f464a155a..82d5f53f9fbd7 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js +++ b/x-pack/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js @@ -5,7 +5,7 @@ */ import { last, isFunction, debounce } from 'lodash'; -import $ from 'plugins/xpack_main/jquery_flot'; +import $ from '../../lib/jquery_flot'; import { DEBOUNCE_FAST_MS } from '../../../common/constants'; /** diff --git a/x-pack/legacy/plugins/monitoring/public/components/status_icon/_index.scss b/x-pack/plugins/monitoring/public/components/status_icon/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/status_icon/_index.scss rename to x-pack/plugins/monitoring/public/components/status_icon/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/status_icon/_status_icon.scss b/x-pack/plugins/monitoring/public/components/status_icon/_status_icon.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/status_icon/_status_icon.scss rename to x-pack/plugins/monitoring/public/components/status_icon/_status_icon.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/status_icon/index.js b/x-pack/plugins/monitoring/public/components/status_icon/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/status_icon/index.js rename to x-pack/plugins/monitoring/public/components/status_icon/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap b/x-pack/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap rename to x-pack/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/_index.scss b/x-pack/plugins/monitoring/public/components/summary_status/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/summary_status/_index.scss rename to x-pack/plugins/monitoring/public/components/summary_status/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/_summary_status.scss b/x-pack/plugins/monitoring/public/components/summary_status/_summary_status.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/summary_status/_summary_status.scss rename to x-pack/plugins/monitoring/public/components/summary_status/_summary_status.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/index.js b/x-pack/plugins/monitoring/public/components/summary_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/summary_status/index.js rename to x-pack/plugins/monitoring/public/components/summary_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/summary_status.js b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/summary_status/summary_status.js rename to x-pack/plugins/monitoring/public/components/summary_status/summary_status.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/summary_status.test.js b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.test.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/summary_status/summary_status.test.js rename to x-pack/plugins/monitoring/public/components/summary_status/summary_status.test.js index fe709af266349..5f4dced47ee6f 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/summary_status/summary_status.test.js +++ b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { SummaryStatus } from './summary_status'; jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/_index.scss b/x-pack/plugins/monitoring/public/components/table/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/table/_index.scss rename to x-pack/plugins/monitoring/public/components/table/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/_table.scss b/x-pack/plugins/monitoring/public/components/table/_table.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/table/_table.scss rename to x-pack/plugins/monitoring/public/components/table/_table.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/eui_table.js b/x-pack/plugins/monitoring/public/components/table/eui_table.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/table/eui_table.js rename to x-pack/plugins/monitoring/public/components/table/eui_table.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/eui_table_ssp.js b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/table/eui_table_ssp.js rename to x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/index.js b/x-pack/plugins/monitoring/public/components/table/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/table/index.js rename to x-pack/plugins/monitoring/public/components/table/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/storage.js b/x-pack/plugins/monitoring/public/components/table/storage.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/table/storage.js rename to x-pack/plugins/monitoring/public/components/table/storage.js diff --git a/x-pack/legacy/plugins/monitoring/public/directives/beats/overview/index.js b/x-pack/plugins/monitoring/public/directives/beats/beat/index.js similarity index 59% rename from x-pack/legacy/plugins/monitoring/public/directives/beats/overview/index.js rename to x-pack/plugins/monitoring/public/directives/beats/beat/index.js index fb78b6a2e0300..103cac98ba564 100644 --- a/x-pack/legacy/plugins/monitoring/public/directives/beats/overview/index.js +++ b/x-pack/plugins/monitoring/public/directives/beats/beat/index.js @@ -6,12 +6,10 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { BeatsOverview } from 'plugins/monitoring/components/beats/overview'; -import { I18nContext } from 'ui/i18n'; +import { Beat } from '../../../components/beats/beat'; -const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringBeatsOverview', () => { +//monitoringBeatsBeat +export function monitoringBeatsBeatProvider() { return { restrict: 'E', scope: { @@ -24,12 +22,15 @@ uiModule.directive('monitoringBeatsOverview', () => { scope.$watch('data', (data = {}) => { render( - - - , + , $el[0] ); }); }, }; -}); +} diff --git a/x-pack/legacy/plugins/monitoring/public/directives/beats/beat/index.js b/x-pack/plugins/monitoring/public/directives/beats/overview/index.js similarity index 55% rename from x-pack/legacy/plugins/monitoring/public/directives/beats/beat/index.js rename to x-pack/plugins/monitoring/public/directives/beats/overview/index.js index c86315fc03482..4faf69e13d02c 100644 --- a/x-pack/legacy/plugins/monitoring/public/directives/beats/beat/index.js +++ b/x-pack/plugins/monitoring/public/directives/beats/overview/index.js @@ -6,12 +6,9 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { Beat } from 'plugins/monitoring/components/beats/beat'; -import { I18nContext } from 'ui/i18n'; +import { BeatsOverview } from '../../../components/beats/overview'; -const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringBeatsBeat', () => { +export function monitoringBeatsOverviewProvider() { return { restrict: 'E', scope: { @@ -24,17 +21,10 @@ uiModule.directive('monitoringBeatsBeat', () => { scope.$watch('data', (data = {}) => { render( - - - , + , $el[0] ); }); }, }; -}); +} diff --git a/x-pack/legacy/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js similarity index 63% rename from x-pack/legacy/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js rename to x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js index 8f35bd599ac49..706d1ac4c0e33 100644 --- a/x-pack/legacy/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js @@ -8,10 +8,8 @@ import { capitalize } from 'lodash'; import numeral from '@elastic/numeral'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nContext } from 'ui/i18n'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; -import { MachineLearningJobStatusIcon } from 'plugins/monitoring/components/elasticsearch/ml_job_listing/status_icon'; +import { EuiMonitoringTable } from '../../../components/table'; +import { MachineLearningJobStatusIcon } from '../../../components/elasticsearch/ml_job_listing/status_icon'; import { LARGE_ABBREVIATED, LARGE_BYTES } from '../../../../common/formatting'; import { EuiLink, EuiPage, EuiPageContent, EuiPageBody, EuiPanel, EuiSpacer } from '@elastic/eui'; import { ClusterStatus } from '../../../components/elasticsearch/cluster_status'; @@ -93,8 +91,8 @@ const getColumns = (kbnUrl, scope) => [ }, ]; -const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringMlListing', kbnUrl => { +//monitoringMlListing +export function monitoringMlListingProvider(kbnUrl) { return { restrict: 'E', scope: { @@ -117,51 +115,49 @@ uiModule.directive('monitoringMlListing', kbnUrl => { scope.$watch('jobs', (jobs = []) => { const mlTable = ( - - - - - - - - - - - - - + + + + + + + + + + + ); render(mlTable, $el[0]); }); }, }; -}); +} diff --git a/x-pack/legacy/plugins/monitoring/public/directives/main/__tests__/monitoring_main_controller.js b/x-pack/plugins/monitoring/public/directives/main/__tests__/monitoring_main_controller.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/directives/main/__tests__/monitoring_main_controller.js rename to x-pack/plugins/monitoring/public/directives/main/__tests__/monitoring_main_controller.js diff --git a/x-pack/legacy/plugins/monitoring/public/directives/main/index.html b/x-pack/plugins/monitoring/public/directives/main/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/directives/main/index.html rename to x-pack/plugins/monitoring/public/directives/main/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/directives/main/index.js b/x-pack/plugins/monitoring/public/directives/main/index.js similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/directives/main/index.js rename to x-pack/plugins/monitoring/public/directives/main/index.js index 4e09225cb85e8..8c28ab6103868 100644 --- a/x-pack/legacy/plugins/monitoring/public/directives/main/index.js +++ b/x-pack/plugins/monitoring/public/directives/main/index.js @@ -8,9 +8,8 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { EuiSelect, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { Legacy } from '../../legacy_shims'; import { shortenPipelineHash } from '../../../common/formatting'; import { getSetupModeState, initSetupModeState } from '../../lib/setup_mode'; import { Subscription } from 'rxjs'; @@ -77,6 +76,7 @@ export class MonitoringMainController { } addTimerangeObservers = () => { + const timefilter = Legacy.shims.timefilter; this.subscriptions = new Subscription(); const refreshIntervalUpdated = () => { @@ -101,6 +101,7 @@ export class MonitoringMainController { // kick things off from the directive link function setup(options) { + const timefilter = Legacy.shims.timefilter; this._licenseService = options.licenseService; this._breadcrumbsService = options.breadcrumbsService; this._kbnUrlService = options.kbnUrlService; @@ -197,8 +198,7 @@ export class MonitoringMainController { } } -const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl, $injector) => { +export function monitoringMainProvider(breadcrumbs, license, kbnUrl, $injector) { const $executor = $injector.get('$executor'); return { @@ -209,7 +209,15 @@ uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl, $injector) = controllerAs: 'monitoringMain', bindToController: true, link(scope, _element, attributes, controller) { - controller.addTimerangeObservers(); + scope.$applyAsync(() => { + controller.addTimerangeObservers(); + const setupObj = getSetupObj(); + controller.setup(setupObj); + Object.keys(setupObj.attributes).forEach(key => { + attributes.$observe(key, () => controller.setup(getSetupObj())); + }); + }); + initSetupModeState(scope, $injector, () => { controller.setup(getSetupObj()); }); @@ -244,11 +252,6 @@ uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl, $injector) = }; } - const setupObj = getSetupObj(); - controller.setup(setupObj); - Object.keys(setupObj.attributes).forEach(key => { - attributes.$observe(key, () => controller.setup(getSetupObj())); - }); scope.$on('$destroy', () => { controller.pipelineDropdownElement && unmountComponentAtNode(controller.pipelineDropdownElement); @@ -260,4 +263,4 @@ uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl, $injector) = }); }, }; -}); +} diff --git a/x-pack/legacy/plugins/monitoring/public/icons/health-gray.svg b/x-pack/plugins/monitoring/public/icons/health-gray.svg similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/icons/health-gray.svg rename to x-pack/plugins/monitoring/public/icons/health-gray.svg diff --git a/x-pack/legacy/plugins/monitoring/public/icons/health-green.svg b/x-pack/plugins/monitoring/public/icons/health-green.svg similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/icons/health-green.svg rename to x-pack/plugins/monitoring/public/icons/health-green.svg diff --git a/x-pack/legacy/plugins/monitoring/public/icons/health-red.svg b/x-pack/plugins/monitoring/public/icons/health-red.svg similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/icons/health-red.svg rename to x-pack/plugins/monitoring/public/icons/health-red.svg diff --git a/x-pack/legacy/plugins/monitoring/public/icons/health-yellow.svg b/x-pack/plugins/monitoring/public/icons/health-yellow.svg similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/icons/health-yellow.svg rename to x-pack/plugins/monitoring/public/icons/health-yellow.svg diff --git a/x-pack/legacy/plugins/monitoring/public/index.scss b/x-pack/plugins/monitoring/public/index.scss similarity index 83% rename from x-pack/legacy/plugins/monitoring/public/index.scss rename to x-pack/plugins/monitoring/public/index.scss index 41bca7774a8b8..4dda80ee7454b 100644 --- a/x-pack/legacy/plugins/monitoring/public/index.scss +++ b/x-pack/plugins/monitoring/public/index.scss @@ -21,3 +21,10 @@ @import 'components/logstash/pipeline_viewer/views/index'; @import 'components/elasticsearch/shard_allocation/index'; @import 'components/setup_mode/index'; +@import 'components/elasticsearch/ccr/index'; + +.monitoringApplicationWrapper { + display: flex; + flex-direction: column; + flex-grow: 1; +} diff --git a/x-pack/legacy/plugins/monitoring/public/np_ready/index.ts b/x-pack/plugins/monitoring/public/index.ts similarity index 84% rename from x-pack/legacy/plugins/monitoring/public/np_ready/index.ts rename to x-pack/plugins/monitoring/public/index.ts index 80848c497c370..71c98e8e8b131 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_ready/index.ts +++ b/x-pack/plugins/monitoring/public/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/public'; +import { PluginInitializerContext } from '../../../../src/core/public'; import { MonitoringPlugin } from './plugin'; export function plugin(ctx: PluginInitializerContext) { diff --git a/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts b/x-pack/plugins/monitoring/public/jest.helpers.ts similarity index 80% rename from x-pack/legacy/plugins/monitoring/public/jest.helpers.ts rename to x-pack/plugins/monitoring/public/jest.helpers.ts index 46ba603d30138..96fb70e480f53 100644 --- a/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts +++ b/x-pack/plugins/monitoring/public/jest.helpers.ts @@ -25,12 +25,3 @@ export function mockUseEffects(count = 1) { spy.mockImplementationOnce(f => f()); } } - -// export function mockUseEffectForDeps(deps, count = 1) { -// const spy = jest.spyOn(React, 'useEffect'); -// for (let i = 0; i < count; i++) { -// spy.mockImplementationOnce((f, depList) => { - -// }); -// } -// } diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts new file mode 100644 index 0000000000000..47aa1048c5130 --- /dev/null +++ b/x-pack/plugins/monitoring/public/legacy_shims.ts @@ -0,0 +1,83 @@ +/* + * 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 { CoreStart } from 'kibana/public'; +import angular from 'angular'; +import { HttpRequestInit } from '../../../../src/core/public'; +import { MonitoringPluginDependencies } from './types'; + +export interface KFetchQuery { + [key: string]: string | number | boolean | undefined; +} + +export interface KFetchOptions extends HttpRequestInit { + pathname: string; + query?: KFetchQuery; + asSystemRequest?: boolean; +} + +export interface KFetchKibanaOptions { + prependBasePath?: boolean; +} + +export interface IShims { + toastNotifications: CoreStart['notifications']['toasts']; + capabilities: { get: () => CoreStart['application']['capabilities'] }; + getAngularInjector: () => angular.auto.IInjectorService; + getBasePath: () => string; + getInjected: (name: string, defaultValue?: unknown) => unknown; + breadcrumbs: { set: () => void }; + I18nContext: CoreStart['i18n']['Context']; + docLinks: CoreStart['docLinks']; + docTitle: CoreStart['chrome']['docTitle']; + timefilter: MonitoringPluginDependencies['data']['query']['timefilter']['timefilter']; + kfetch: ( + { pathname, ...options }: KFetchOptions, + kfetchOptions?: KFetchKibanaOptions | undefined + ) => Promise; + isCloud: boolean; +} + +export class Legacy { + private static _shims: IShims; + + public static init( + { core, data, isCloud }: MonitoringPluginDependencies, + ngInjector: angular.auto.IInjectorService + ) { + this._shims = { + toastNotifications: core.notifications.toasts, + capabilities: { get: () => core.application.capabilities }, + getAngularInjector: (): angular.auto.IInjectorService => ngInjector, + getBasePath: (): string => core.http.basePath.get(), + getInjected: (name: string, defaultValue?: unknown): string | unknown => + core.injectedMetadata.getInjectedVar(name, defaultValue), + breadcrumbs: { + set: (...args: any[0]) => core.chrome.setBreadcrumbs.apply(this, args), + }, + I18nContext: core.i18n.Context, + docLinks: core.docLinks, + docTitle: core.chrome.docTitle, + timefilter: data.query.timefilter.timefilter, + kfetch: async ( + { pathname, ...options }: KFetchOptions, + kfetchOptions?: KFetchKibanaOptions + ) => + await core.http.fetch(pathname, { + prependBasePath: kfetchOptions?.prependBasePath, + ...options, + }), + isCloud, + }; + } + + public static get shims(): Readonly { + if (!Legacy._shims) { + throw new Error('Legacy needs to be initiated with Legacy.init(...) before use'); + } + return Legacy._shims; + } +} diff --git a/x-pack/legacy/plugins/monitoring/public/lib/__tests__/format_number.js b/x-pack/plugins/monitoring/public/lib/__tests__/format_number.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/__tests__/format_number.js rename to x-pack/plugins/monitoring/public/lib/__tests__/format_number.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx b/x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx similarity index 88% rename from x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx rename to x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx index c09014b9a9627..d729457f60df1 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx +++ b/x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx @@ -6,12 +6,11 @@ import React from 'react'; import { contains } from 'lodash'; -import { toastNotifications } from 'ui/notify'; -// @ts-ignore -import { formatMsg } from '../../../../../../src/plugins/kibana_legacy/public'; // eslint-disable-line import/order import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import { Legacy } from '../legacy_shims'; +import { formatMsg } from '../../../../../src/plugins/kibana_legacy/public'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; export function formatMonitoringError(err: any) { // TODO: We should stop using Boom for errors and instead write a custom handler to return richer error objects @@ -43,7 +42,7 @@ export function ajaxErrorHandlersProvider($injector: any) { kbnUrl.redirect('access-denied'); } else if (err.status === 404 && !contains(window.location.hash, 'no-data')) { // pass through if this is a 404 and we're already on the no-data page - toastNotifications.addDanger({ + Legacy.shims.toastNotifications.addDanger({ title: toMountPoint( string) + labelBoxBorderColor: color + noColumns: number + position: "ne" or "nw" or "se" or "sw" + margin: number of pixels or [x margin, y margin] + backgroundColor: null or color + backgroundOpacity: number between 0 and 1 + container: null or jQuery object/DOM element/jQuery expression + sorted: null/false, true, "ascending", "descending", "reverse", or a comparator +} +``` + +The legend is generated as a table with the data series labels and +small label boxes with the color of the series. If you want to format +the labels in some way, e.g. make them to links, you can pass in a +function for "labelFormatter". Here's an example that makes them +clickable: + +```js +labelFormatter: function(label, series) { + // series is the series object for the label + return '
' + label + ''; +} +``` + +To prevent a series from showing up in the legend, simply have the function +return null. + +"noColumns" is the number of columns to divide the legend table into. +"position" specifies the overall placement of the legend within the +plot (top-right, top-left, etc.) and margin the distance to the plot +edge (this can be either a number or an array of two numbers like [x, +y]). "backgroundColor" and "backgroundOpacity" specifies the +background. The default is a partly transparent auto-detected +background. + +If you want the legend to appear somewhere else in the DOM, you can +specify "container" as a jQuery object/expression to put the legend +table into. The "position" and "margin" etc. options will then be +ignored. Note that Flot will overwrite the contents of the container. + +Legend entries appear in the same order as their series by default. If "sorted" +is "reverse" then they appear in the opposite order from their series. To sort +them alphabetically, you can specify true, "ascending" or "descending", where +true and "ascending" are equivalent. + +You can also provide your own comparator function that accepts two +objects with "label" and "color" properties, and returns zero if they +are equal, a positive value if the first is greater than the second, +and a negative value if the first is less than the second. + +```js +sorted: function(a, b) { + // sort alphabetically in ascending order + return a.label == b.label ? 0 : ( + a.label > b.label ? 1 : -1 + ) +} +``` + + +## Customizing the axes ## + +```js +xaxis, yaxis: { + show: null or true/false + position: "bottom" or "top" or "left" or "right" + mode: null or "time" ("time" requires jquery.flot.time.js plugin) + timezone: null, "browser" or timezone (only makes sense for mode: "time") + + color: null or color spec + tickColor: null or color spec + font: null or font spec object + + min: null or number + max: null or number + autoscaleMargin: null or number + + transform: null or fn: number -> number + inverseTransform: null or fn: number -> number + + ticks: null or number or ticks array or (fn: axis -> ticks array) + tickSize: number or array + minTickSize: number or array + tickFormatter: (fn: number, object -> string) or string + tickDecimals: null or number + + labelWidth: null or number + labelHeight: null or number + reserveSpace: null or true + + tickLength: null or number + + alignTicksWithAxis: null or number +} +``` + +All axes have the same kind of options. The following describes how to +configure one axis, see below for what to do if you've got more than +one x axis or y axis. + +If you don't set the "show" option (i.e. it is null), visibility is +auto-detected, i.e. the axis will show up if there's data associated +with it. You can override this by setting the "show" option to true or +false. + +The "position" option specifies where the axis is placed, bottom or +top for x axes, left or right for y axes. The "mode" option determines +how the data is interpreted, the default of null means as decimal +numbers. Use "time" for time series data; see the time series data +section. The time plugin (jquery.flot.time.js) is required for time +series support. + +The "color" option determines the color of the line and ticks for the axis, and +defaults to the grid color with transparency. For more fine-grained control you +can also set the color of the ticks separately with "tickColor". + +You can customize the font and color used to draw the axis tick labels with CSS +or directly via the "font" option. When "font" is null - the default - each +tick label is given the 'flot-tick-label' class. For compatibility with Flot +0.7 and earlier the labels are also given the 'tickLabel' class, but this is +deprecated and scheduled to be removed with the release of version 1.0.0. + +To enable more granular control over styles, labels are divided between a set +of text containers, with each holding the labels for one axis. These containers +are given the classes 'flot-[x|y]-axis', and 'flot-[x|y]#-axis', where '#' is +the number of the axis when there are multiple axes. For example, the x-axis +labels for a simple plot with only a single x-axis might look like this: + +```html +
+
January 2013
+ ... +
+``` + +For direct control over label styles you can also provide "font" as an object +with this format: + +```js +{ + size: 11, + lineHeight: 13, + style: "italic", + weight: "bold", + family: "sans-serif", + variant: "small-caps", + color: "#545454" +} +``` + +The size and lineHeight must be expressed in pixels; CSS units such as 'em' +or 'smaller' are not allowed. + +The options "min"/"max" are the precise minimum/maximum value on the +scale. If you don't specify either of them, a value will automatically +be chosen based on the minimum/maximum data values. Note that Flot +always examines all the data values you feed to it, even if a +restriction on another axis may make some of them invisible (this +makes interactive use more stable). + +The "autoscaleMargin" is a bit esoteric: it's the fraction of margin +that the scaling algorithm will add to avoid that the outermost points +ends up on the grid border. Note that this margin is only applied when +a min or max value is not explicitly set. If a margin is specified, +the plot will furthermore extend the axis end-point to the nearest +whole tick. The default value is "null" for the x axes and 0.02 for y +axes which seems appropriate for most cases. + +"transform" and "inverseTransform" are callbacks you can put in to +change the way the data is drawn. You can design a function to +compress or expand certain parts of the axis non-linearly, e.g. +suppress weekends or compress far away points with a logarithm or some +other means. When Flot draws the plot, each value is first put through +the transform function. Here's an example, the x axis can be turned +into a natural logarithm axis with the following code: + +```js +xaxis: { + transform: function (v) { return Math.log(v); }, + inverseTransform: function (v) { return Math.exp(v); } +} +``` + +Similarly, for reversing the y axis so the values appear in inverse +order: + +```js +yaxis: { + transform: function (v) { return -v; }, + inverseTransform: function (v) { return -v; } +} +``` + +Note that for finding extrema, Flot assumes that the transform +function does not reorder values (it should be monotone). + +The inverseTransform is simply the inverse of the transform function +(so v == inverseTransform(transform(v)) for all relevant v). It is +required for converting from canvas coordinates to data coordinates, +e.g. for a mouse interaction where a certain pixel is clicked. If you +don't use any interactive features of Flot, you may not need it. + + +The rest of the options deal with the ticks. + +If you don't specify any ticks, a tick generator algorithm will make +some for you. The algorithm has two passes. It first estimates how +many ticks would be reasonable and uses this number to compute a nice +round tick interval size. Then it generates the ticks. + +You can specify how many ticks the algorithm aims for by setting +"ticks" to a number. The algorithm always tries to generate reasonably +round tick values so even if you ask for three ticks, you might get +five if that fits better with the rounding. If you don't want any +ticks at all, set "ticks" to 0 or an empty array. + +Another option is to skip the rounding part and directly set the tick +interval size with "tickSize". If you set it to 2, you'll get ticks at +2, 4, 6, etc. Alternatively, you can specify that you just don't want +ticks at a size less than a specific tick size with "minTickSize". +Note that for time series, the format is an array like [2, "month"], +see the next section. + +If you want to completely override the tick algorithm, you can specify +an array for "ticks", either like this: + +```js +ticks: [0, 1.2, 2.4] +``` + +Or like this where the labels are also customized: + +```js +ticks: [[0, "zero"], [1.2, "one mark"], [2.4, "two marks"]] +``` + +You can mix the two if you like. + +For extra flexibility you can specify a function as the "ticks" +parameter. The function will be called with an object with the axis +min and max and should return a ticks array. Here's a simplistic tick +generator that spits out intervals of pi, suitable for use on the x +axis for trigonometric functions: + +```js +function piTickGenerator(axis) { + var res = [], i = Math.floor(axis.min / Math.PI); + do { + var v = i * Math.PI; + res.push([v, i + "\u03c0"]); + ++i; + } while (v < axis.max); + return res; +} +``` + +You can control how the ticks look like with "tickDecimals", the +number of decimals to display (default is auto-detected). + +Alternatively, for ultimate control over how ticks are formatted you can +provide a function to "tickFormatter". The function is passed two +parameters, the tick value and an axis object with information, and +should return a string. The default formatter looks like this: + +```js +function formatter(val, axis) { + return val.toFixed(axis.tickDecimals); +} +``` + +The axis object has "min" and "max" with the range of the axis, +"tickDecimals" with the number of decimals to round the value to and +"tickSize" with the size of the interval between ticks as calculated +by the automatic axis scaling algorithm (or specified by you). Here's +an example of a custom formatter: + +```js +function suffixFormatter(val, axis) { + if (val > 1000000) + return (val / 1000000).toFixed(axis.tickDecimals) + " MB"; + else if (val > 1000) + return (val / 1000).toFixed(axis.tickDecimals) + " kB"; + else + return val.toFixed(axis.tickDecimals) + " B"; +} +``` + +"labelWidth" and "labelHeight" specifies a fixed size of the tick +labels in pixels. They're useful in case you need to align several +plots. "reserveSpace" means that even if an axis isn't shown, Flot +should reserve space for it - it is useful in combination with +labelWidth and labelHeight for aligning multi-axis charts. + +"tickLength" is the length of the tick lines in pixels. By default, the +innermost axes will have ticks that extend all across the plot, while +any extra axes use small ticks. A value of null means use the default, +while a number means small ticks of that length - set it to 0 to hide +the lines completely. + +If you set "alignTicksWithAxis" to the number of another axis, e.g. +alignTicksWithAxis: 1, Flot will ensure that the autogenerated ticks +of this axis are aligned with the ticks of the other axis. This may +improve the looks, e.g. if you have one y axis to the left and one to +the right, because the grid lines will then match the ticks in both +ends. The trade-off is that the forced ticks won't necessarily be at +natural places. + + +## Multiple axes ## + +If you need more than one x axis or y axis, you need to specify for +each data series which axis they are to use, as described under the +format of the data series, e.g. { data: [...], yaxis: 2 } specifies +that a series should be plotted against the second y axis. + +To actually configure that axis, you can't use the xaxis/yaxis options +directly - instead there are two arrays in the options: + +```js +xaxes: [] +yaxes: [] +``` + +Here's an example of configuring a single x axis and two y axes (we +can leave options of the first y axis empty as the defaults are fine): + +```js +{ + xaxes: [ { position: "top" } ], + yaxes: [ { }, { position: "right", min: 20 } ] +} +``` + +The arrays get their default values from the xaxis/yaxis settings, so +say you want to have all y axes start at zero, you can simply specify +yaxis: { min: 0 } instead of adding a min parameter to all the axes. + +Generally, the various interfaces in Flot dealing with data points +either accept an xaxis/yaxis parameter to specify which axis number to +use (starting from 1), or lets you specify the coordinate directly as +x2/x3/... or x2axis/x3axis/... instead of "x" or "xaxis". + + +## Time series data ## + +Please note that it is now required to include the time plugin, +jquery.flot.time.js, for time series support. + +Time series are a bit more difficult than scalar data because +calendars don't follow a simple base 10 system. For many cases, Flot +abstracts most of this away, but it can still be a bit difficult to +get the data into Flot. So we'll first discuss the data format. + +The time series support in Flot is based on JavaScript timestamps, +i.e. everywhere a time value is expected or handed over, a JavaScript +timestamp number is used. This is a number, not a Date object. A +JavaScript timestamp is the number of milliseconds since January 1, +1970 00:00:00 UTC. This is almost the same as Unix timestamps, except it's +in milliseconds, so remember to multiply by 1000! + +You can see a timestamp like this + +```js +alert((new Date()).getTime()) +``` + +There are different schools of thought when it comes to display of +timestamps. Many will want the timestamps to be displayed according to +a certain time zone, usually the time zone in which the data has been +produced. Some want the localized experience, where the timestamps are +displayed according to the local time of the visitor. Flot supports +both. Optionally you can include a third-party library to get +additional timezone support. + +Default behavior is that Flot always displays timestamps according to +UTC. The reason being that the core JavaScript Date object does not +support other fixed time zones. Often your data is at another time +zone, so it may take a little bit of tweaking to work around this +limitation. + +The easiest way to think about it is to pretend that the data +production time zone is UTC, even if it isn't. So if you have a +datapoint at 2002-02-20 08:00, you can generate a timestamp for eight +o'clock UTC even if it really happened eight o'clock UTC+0200. + +In PHP you can get an appropriate timestamp with: + +```php +strtotime("2002-02-20 UTC") * 1000 +``` + +In Python you can get it with something like: + +```python +calendar.timegm(datetime_object.timetuple()) * 1000 +``` +In Ruby you can get it using the `#to_i` method on the +[`Time`](http://apidock.com/ruby/Time/to_i) object. If you're using the +`active_support` gem (default for Ruby on Rails applications) `#to_i` is also +available on the `DateTime` and `ActiveSupport::TimeWithZone` objects. You +simply need to multiply the result by 1000: + +```ruby +Time.now.to_i * 1000 # => 1383582043000 +# ActiveSupport examples: +DateTime.now.to_i * 1000 # => 1383582043000 +ActiveSupport::TimeZone.new('Asia/Shanghai').now.to_i * 1000 +# => 1383582043000 +``` + +In .NET you can get it with something like: + +```aspx +public static int GetJavaScriptTimestamp(System.DateTime input) +{ + System.TimeSpan span = new System.TimeSpan(System.DateTime.Parse("1/1/1970").Ticks); + System.DateTime time = input.Subtract(span); + return (long)(time.Ticks / 10000); +} +``` + +JavaScript also has some support for parsing date strings, so it is +possible to generate the timestamps manually client-side. + +If you've already got the real UTC timestamp, it's too late to use the +pretend trick described above. But you can fix up the timestamps by +adding the time zone offset, e.g. for UTC+0200 you would add 2 hours +to the UTC timestamp you got. Then it'll look right on the plot. Most +programming environments have some means of getting the timezone +offset for a specific date (note that you need to get the offset for +each individual timestamp to account for daylight savings). + +The alternative with core JavaScript is to interpret the timestamps +according to the time zone that the visitor is in, which means that +the ticks will shift with the time zone and daylight savings of each +visitor. This behavior is enabled by setting the axis option +"timezone" to the value "browser". + +If you need more time zone functionality than this, there is still +another option. If you include the "timezone-js" library + in the page and set axis.timezone +to a value recognized by said library, Flot will use timezone-js to +interpret the timestamps according to that time zone. + +Once you've gotten the timestamps into the data and specified "time" +as the axis mode, Flot will automatically generate relevant ticks and +format them. As always, you can tweak the ticks via the "ticks" option +- just remember that the values should be timestamps (numbers), not +Date objects. + +Tick generation and formatting can also be controlled separately +through the following axis options: + +```js +minTickSize: array +timeformat: null or format string +monthNames: null or array of size 12 of strings +dayNames: null or array of size 7 of strings +twelveHourClock: boolean +``` + +Here "timeformat" is a format string to use. You might use it like +this: + +```js +xaxis: { + mode: "time", + timeformat: "%Y/%m/%d" +} +``` + +This will result in tick labels like "2000/12/24". A subset of the +standard strftime specifiers are supported (plus the nonstandard %q): + +```js +%a: weekday name (customizable) +%b: month name (customizable) +%d: day of month, zero-padded (01-31) +%e: day of month, space-padded ( 1-31) +%H: hours, 24-hour time, zero-padded (00-23) +%I: hours, 12-hour time, zero-padded (01-12) +%m: month, zero-padded (01-12) +%M: minutes, zero-padded (00-59) +%q: quarter (1-4) +%S: seconds, zero-padded (00-59) +%y: year (two digits) +%Y: year (four digits) +%p: am/pm +%P: AM/PM (uppercase version of %p) +%w: weekday as number (0-6, 0 being Sunday) +``` + +Flot 0.8 switched from %h to the standard %H hours specifier. The %h specifier +is still available, for backwards-compatibility, but is deprecated and +scheduled to be removed permanently with the release of version 1.0. + +You can customize the month names with the "monthNames" option. For +instance, for Danish you might specify: + +```js +monthNames: ["jan", "feb", "mar", "apr", "maj", "jun", "jul", "aug", "sep", "okt", "nov", "dec"] +``` + +Similarly you can customize the weekday names with the "dayNames" +option. An example in French: + +```js +dayNames: ["dim", "lun", "mar", "mer", "jeu", "ven", "sam"] +``` + +If you set "twelveHourClock" to true, the autogenerated timestamps +will use 12 hour AM/PM timestamps instead of 24 hour. This only +applies if you have not set "timeformat". Use the "%I" and "%p" or +"%P" options if you want to build your own format string with 12-hour +times. + +If the Date object has a strftime property (and it is a function), it +will be used instead of the built-in formatter. Thus you can include +a strftime library such as http://hacks.bluesmoon.info/strftime/ for +more powerful date/time formatting. + +If everything else fails, you can control the formatting by specifying +a custom tick formatter function as usual. Here's a simple example +which will format December 24 as 24/12: + +```js +tickFormatter: function (val, axis) { + var d = new Date(val); + return d.getUTCDate() + "/" + (d.getUTCMonth() + 1); +} +``` + +Note that for the time mode "tickSize" and "minTickSize" are a bit +special in that they are arrays on the form "[value, unit]" where unit +is one of "second", "minute", "hour", "day", "month" and "year". So +you can specify + +```js +minTickSize: [1, "month"] +``` + +to get a tick interval size of at least 1 month and correspondingly, +if axis.tickSize is [2, "day"] in the tick formatter, the ticks have +been produced with two days in-between. + + +## Customizing the data series ## + +```js +series: { + lines, points, bars: { + show: boolean + lineWidth: number + fill: boolean or number + fillColor: null or color/gradient + } + + lines, bars: { + zero: boolean + } + + points: { + radius: number + symbol: "circle" or function + } + + bars: { + barWidth: number + align: "left", "right" or "center" + horizontal: boolean + } + + lines: { + steps: boolean + } + + shadowSize: number + highlightColor: color or number +} + +colors: [ color1, color2, ... ] +``` + +The options inside "series: {}" are copied to each of the series. So +you can specify that all series should have bars by putting it in the +global options, or override it for individual series by specifying +bars in a particular the series object in the array of data. + +The most important options are "lines", "points" and "bars" that +specify whether and how lines, points and bars should be shown for +each data series. In case you don't specify anything at all, Flot will +default to showing lines (you can turn this off with +lines: { show: false }). You can specify the various types +independently of each other, and Flot will happily draw each of them +in turn (this is probably only useful for lines and points), e.g. + +```js +var options = { + series: { + lines: { show: true, fill: true, fillColor: "rgba(255, 255, 255, 0.8)" }, + points: { show: true, fill: false } + } +}; +``` + +"lineWidth" is the thickness of the line or outline in pixels. You can +set it to 0 to prevent a line or outline from being drawn; this will +also hide the shadow. + +"fill" is whether the shape should be filled. For lines, this produces +area graphs. You can use "fillColor" to specify the color of the fill. +If "fillColor" evaluates to false (default for everything except +points which are filled with white), the fill color is auto-set to the +color of the data series. You can adjust the opacity of the fill by +setting fill to a number between 0 (fully transparent) and 1 (fully +opaque). + +For bars, fillColor can be a gradient, see the gradient documentation +below. "barWidth" is the width of the bars in units of the x axis (or +the y axis if "horizontal" is true), contrary to most other measures +that are specified in pixels. For instance, for time series the unit +is milliseconds so 24 * 60 * 60 * 1000 produces bars with the width of +a day. "align" specifies whether a bar should be left-aligned +(default), right-aligned or centered on top of the value it represents. +When "horizontal" is on, the bars are drawn horizontally, i.e. from the +y axis instead of the x axis; note that the bar end points are still +defined in the same way so you'll probably want to swap the +coordinates if you've been plotting vertical bars first. + +Area and bar charts normally start from zero, regardless of the data's range. +This is because they convey information through size, and starting from a +different value would distort their meaning. In cases where the fill is purely +for decorative purposes, however, "zero" allows you to override this behavior. +It defaults to true for filled lines and bars; setting it to false tells the +series to use the same automatic scaling as an un-filled line. + +For lines, "steps" specifies whether two adjacent data points are +connected with a straight (possibly diagonal) line or with first a +horizontal and then a vertical line. Note that this transforms the +data by adding extra points. + +For points, you can specify the radius and the symbol. The only +built-in symbol type is circles, for other types you can use a plugin +or define them yourself by specifying a callback: + +```js +function cross(ctx, x, y, radius, shadow) { + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.moveTo(x - size, y - size); + ctx.lineTo(x + size, y + size); + ctx.moveTo(x - size, y + size); + ctx.lineTo(x + size, y - size); +} +``` + +The parameters are the drawing context, x and y coordinates of the +center of the point, a radius which corresponds to what the circle +would have used and whether the call is to draw a shadow (due to +limited canvas support, shadows are currently faked through extra +draws). It's good practice to ensure that the area covered by the +symbol is the same as for the circle with the given radius, this +ensures that all symbols have approximately the same visual weight. + +"shadowSize" is the default size of shadows in pixels. Set it to 0 to +remove shadows. + +"highlightColor" is the default color of the translucent overlay used +to highlight the series when the mouse hovers over it. + +The "colors" array specifies a default color theme to get colors for +the data series from. You can specify as many colors as you like, like +this: + +```js +colors: ["#d18b2c", "#dba255", "#919733"] +``` + +If there are more data series than colors, Flot will try to generate +extra colors by lightening and darkening colors in the theme. + + +## Customizing the grid ## + +```js +grid: { + show: boolean + aboveData: boolean + color: color + backgroundColor: color/gradient or null + margin: number or margin object + labelMargin: number + axisMargin: number + markings: array of markings or (fn: axes -> array of markings) + borderWidth: number or object with "top", "right", "bottom" and "left" properties with different widths + borderColor: color or null or object with "top", "right", "bottom" and "left" properties with different colors + minBorderMargin: number or null + clickable: boolean + hoverable: boolean + autoHighlight: boolean + mouseActiveRadius: number +} + +interaction: { + redrawOverlayInterval: number or -1 +} +``` + +The grid is the thing with the axes and a number of ticks. Many of the +things in the grid are configured under the individual axes, but not +all. "color" is the color of the grid itself whereas "backgroundColor" +specifies the background color inside the grid area, here null means +that the background is transparent. You can also set a gradient, see +the gradient documentation below. + +You can turn off the whole grid including tick labels by setting +"show" to false. "aboveData" determines whether the grid is drawn +above the data or below (below is default). + +"margin" is the space in pixels between the canvas edge and the grid, +which can be either a number or an object with individual margins for +each side, in the form: + +```js +margin: { + top: top margin in pixels + left: left margin in pixels + bottom: bottom margin in pixels + right: right margin in pixels +} +``` + +"labelMargin" is the space in pixels between tick labels and axis +line, and "axisMargin" is the space in pixels between axes when there +are two next to each other. + +"borderWidth" is the width of the border around the plot. Set it to 0 +to disable the border. Set it to an object with "top", "right", +"bottom" and "left" properties to use different widths. You can +also set "borderColor" if you want the border to have a different color +than the grid lines. Set it to an object with "top", "right", "bottom" +and "left" properties to use different colors. "minBorderMargin" controls +the default minimum margin around the border - it's used to make sure +that points aren't accidentally clipped by the canvas edge so by default +the value is computed from the point radius. + +"markings" is used to draw simple lines and rectangular areas in the +background of the plot. You can either specify an array of ranges on +the form { xaxis: { from, to }, yaxis: { from, to } } (with multiple +axes, you can specify coordinates for other axes instead, e.g. as +x2axis/x3axis/...) or with a function that returns such an array given +the axes for the plot in an object as the first parameter. + +You can set the color of markings by specifying "color" in the ranges +object. Here's an example array: + +```js +markings: [ { xaxis: { from: 0, to: 2 }, yaxis: { from: 10, to: 10 }, color: "#bb0000" }, ... ] +``` + +If you leave out one of the values, that value is assumed to go to the +border of the plot. So for example if you only specify { xaxis: { +from: 0, to: 2 } } it means an area that extends from the top to the +bottom of the plot in the x range 0-2. + +A line is drawn if from and to are the same, e.g. + +```js +markings: [ { yaxis: { from: 1, to: 1 } }, ... ] +``` + +would draw a line parallel to the x axis at y = 1. You can control the +line width with "lineWidth" in the range object. + +An example function that makes vertical stripes might look like this: + +```js +markings: function (axes) { + var markings = []; + for (var x = Math.floor(axes.xaxis.min); x < axes.xaxis.max; x += 2) + markings.push({ xaxis: { from: x, to: x + 1 } }); + return markings; +} +``` + +If you set "clickable" to true, the plot will listen for click events +on the plot area and fire a "plotclick" event on the placeholder with +a position and a nearby data item object as parameters. The coordinates +are available both in the unit of the axes (not in pixels) and in +global screen coordinates. + +Likewise, if you set "hoverable" to true, the plot will listen for +mouse move events on the plot area and fire a "plothover" event with +the same parameters as the "plotclick" event. If "autoHighlight" is +true (the default), nearby data items are highlighted automatically. +If needed, you can disable highlighting and control it yourself with +the highlight/unhighlight plot methods described elsewhere. + +You can use "plotclick" and "plothover" events like this: + +```js +$.plot($("#placeholder"), [ d ], { grid: { clickable: true } }); + +$("#placeholder").bind("plotclick", function (event, pos, item) { + alert("You clicked at " + pos.x + ", " + pos.y); + // axis coordinates for other axes, if present, are in pos.x2, pos.x3, ... + // if you need global screen coordinates, they are pos.pageX, pos.pageY + + if (item) { + highlight(item.series, item.datapoint); + alert("You clicked a point!"); + } +}); +``` + +The item object in this example is either null or a nearby object on the form: + +```js +item: { + datapoint: the point, e.g. [0, 2] + dataIndex: the index of the point in the data array + series: the series object + seriesIndex: the index of the series + pageX, pageY: the global screen coordinates of the point +} +``` + +For instance, if you have specified the data like this + +```js +$.plot($("#placeholder"), [ { label: "Foo", data: [[0, 10], [7, 3]] } ], ...); +``` + +and the mouse is near the point (7, 3), "datapoint" is [7, 3], +"dataIndex" will be 1, "series" is a normalized series object with +among other things the "Foo" label in series.label and the color in +series.color, and "seriesIndex" is 0. Note that plugins and options +that transform the data can shift the indexes from what you specified +in the original data array. + +If you use the above events to update some other information and want +to clear out that info in case the mouse goes away, you'll probably +also need to listen to "mouseout" events on the placeholder div. + +"mouseActiveRadius" specifies how far the mouse can be from an item +and still activate it. If there are two or more points within this +radius, Flot chooses the closest item. For bars, the top-most bar +(from the latest specified data series) is chosen. + +If you want to disable interactivity for a specific data series, you +can set "hoverable" and "clickable" to false in the options for that +series, like this: + +```js +{ data: [...], label: "Foo", clickable: false } +``` + +"redrawOverlayInterval" specifies the maximum time to delay a redraw +of interactive things (this works as a rate limiting device). The +default is capped to 60 frames per second. You can set it to -1 to +disable the rate limiting. + + +## Specifying gradients ## + +A gradient is specified like this: + +```js +{ colors: [ color1, color2, ... ] } +``` + +For instance, you might specify a background on the grid going from +black to gray like this: + +```js +grid: { + backgroundColor: { colors: ["#000", "#999"] } +} +``` + +For the series you can specify the gradient as an object that +specifies the scaling of the brightness and the opacity of the series +color, e.g. + +```js +{ colors: [{ opacity: 0.8 }, { brightness: 0.6, opacity: 0.8 } ] } +``` + +where the first color simply has its alpha scaled, whereas the second +is also darkened. For instance, for bars the following makes the bars +gradually disappear, without outline: + +```js +bars: { + show: true, + lineWidth: 0, + fill: true, + fillColor: { colors: [ { opacity: 0.8 }, { opacity: 0.1 } ] } +} +``` + +Flot currently only supports vertical gradients drawn from top to +bottom because that's what works with IE. + + +## Plot Methods ## + +The Plot object returned from the plot function has some methods you +can call: + + - highlight(series, datapoint) + + Highlight a specific datapoint in the data series. You can either + specify the actual objects, e.g. if you got them from a + "plotclick" event, or you can specify the indices, e.g. + highlight(1, 3) to highlight the fourth point in the second series + (remember, zero-based indexing). + + - unhighlight(series, datapoint) or unhighlight() + + Remove the highlighting of the point, same parameters as + highlight. + + If you call unhighlight with no parameters, e.g. as + plot.unhighlight(), all current highlights are removed. + + - setData(data) + + You can use this to reset the data used. Note that axis scaling, + ticks, legend etc. will not be recomputed (use setupGrid() to do + that). You'll probably want to call draw() afterwards. + + You can use this function to speed up redrawing a small plot if + you know that the axes won't change. Put in the new data with + setData(newdata), call draw(), and you're good to go. Note that + for large datasets, almost all the time is consumed in draw() + plotting the data so in this case don't bother. + + - setupGrid() + + Recalculate and set axis scaling, ticks, legend etc. + + Note that because of the drawing model of the canvas, this + function will immediately redraw (actually reinsert in the DOM) + the labels and the legend, but not the actual tick lines because + they're drawn on the canvas. You need to call draw() to get the + canvas redrawn. + + - draw() + + Redraws the plot canvas. + + - triggerRedrawOverlay() + + Schedules an update of an overlay canvas used for drawing + interactive things like a selection and point highlights. This + is mostly useful for writing plugins. The redraw doesn't happen + immediately, instead a timer is set to catch multiple successive + redraws (e.g. from a mousemove). You can get to the overlay by + setting up a drawOverlay hook. + + - width()/height() + + Gets the width and height of the plotting area inside the grid. + This is smaller than the canvas or placeholder dimensions as some + extra space is needed (e.g. for labels). + + - offset() + + Returns the offset of the plotting area inside the grid relative + to the document, useful for instance for calculating mouse + positions (event.pageX/Y minus this offset is the pixel position + inside the plot). + + - pointOffset({ x: xpos, y: ypos }) + + Returns the calculated offset of the data point at (x, y) in data + space within the placeholder div. If you are working with multiple + axes, you can specify the x and y axis references, e.g. + + ```js + o = pointOffset({ x: xpos, y: ypos, xaxis: 2, yaxis: 3 }) + // o.left and o.top now contains the offset within the div + ```` + + - resize() + + Tells Flot to resize the drawing canvas to the size of the + placeholder. You need to run setupGrid() and draw() afterwards as + canvas resizing is a destructive operation. This is used + internally by the resize plugin. + + - shutdown() + + Cleans up any event handlers Flot has currently registered. This + is used internally. + +There are also some members that let you peek inside the internal +workings of Flot which is useful in some cases. Note that if you change +something in the objects returned, you're changing the objects used by +Flot to keep track of its state, so be careful. + + - getData() + + Returns an array of the data series currently used in normalized + form with missing settings filled in according to the global + options. So for instance to find out what color Flot has assigned + to the data series, you could do this: + + ```js + var series = plot.getData(); + for (var i = 0; i < series.length; ++i) + alert(series[i].color); + ``` + + A notable other interesting field besides color is datapoints + which has a field "points" with the normalized data points in a + flat array (the field "pointsize" is the increment in the flat + array to get to the next point so for a dataset consisting only of + (x,y) pairs it would be 2). + + - getAxes() + + Gets an object with the axes. The axes are returned as the + attributes of the object, so for instance getAxes().xaxis is the + x axis. + + Various things are stuffed inside an axis object, e.g. you could + use getAxes().xaxis.ticks to find out what the ticks are for the + xaxis. Two other useful attributes are p2c and c2p, functions for + transforming from data point space to the canvas plot space and + back. Both returns values that are offset with the plot offset. + Check the Flot source code for the complete set of attributes (or + output an axis with console.log() and inspect it). + + With multiple axes, the extra axes are returned as x2axis, x3axis, + etc., e.g. getAxes().y2axis is the second y axis. You can check + y2axis.used to see whether the axis is associated with any data + points and y2axis.show to see if it is currently shown. + + - getPlaceholder() + + Returns placeholder that the plot was put into. This can be useful + for plugins for adding DOM elements or firing events. + + - getCanvas() + + Returns the canvas used for drawing in case you need to hack on it + yourself. You'll probably need to get the plot offset too. + + - getPlotOffset() + + Gets the offset that the grid has within the canvas as an object + with distances from the canvas edges as "left", "right", "top", + "bottom". I.e., if you draw a circle on the canvas with the center + placed at (left, top), its center will be at the top-most, left + corner of the grid. + + - getOptions() + + Gets the options for the plot, normalized, with default values + filled in. You get a reference to actual values used by Flot, so + if you modify the values in here, Flot will use the new values. + If you change something, you probably have to call draw() or + setupGrid() or triggerRedrawOverlay() to see the change. + + +## Hooks ## + +In addition to the public methods, the Plot object also has some hooks +that can be used to modify the plotting process. You can install a +callback function at various points in the process, the function then +gets access to the internal data structures in Flot. + +Here's an overview of the phases Flot goes through: + + 1. Plugin initialization, parsing options + + 2. Constructing the canvases used for drawing + + 3. Set data: parsing data specification, calculating colors, + copying raw data points into internal format, + normalizing them, finding max/min for axis auto-scaling + + 4. Grid setup: calculating axis spacing, ticks, inserting tick + labels, the legend + + 5. Draw: drawing the grid, drawing each of the series in turn + + 6. Setting up event handling for interactive features + + 7. Responding to events, if any + + 8. Shutdown: this mostly happens in case a plot is overwritten + +Each hook is simply a function which is put in the appropriate array. +You can add them through the "hooks" option, and they are also available +after the plot is constructed as the "hooks" attribute on the returned +plot object, e.g. + +```js + // define a simple draw hook + function hellohook(plot, canvascontext) { alert("hello!"); }; + + // pass it in, in an array since we might want to specify several + var plot = $.plot(placeholder, data, { hooks: { draw: [hellohook] } }); + + // we can now find it again in plot.hooks.draw[0] unless a plugin + // has added other hooks +``` + +The available hooks are described below. All hook callbacks get the +plot object as first parameter. You can find some examples of defined +hooks in the plugins bundled with Flot. + + - processOptions [phase 1] + + ```function(plot, options)``` + + Called after Flot has parsed and merged options. Useful in the + instance where customizations beyond simple merging of default + values is needed. A plugin might use it to detect that it has been + enabled and then turn on or off other options. + + + - processRawData [phase 3] + + ```function(plot, series, data, datapoints)``` + + Called before Flot copies and normalizes the raw data for the given + series. If the function fills in datapoints.points with normalized + points and sets datapoints.pointsize to the size of the points, + Flot will skip the copying/normalization step for this series. + + In any case, you might be interested in setting datapoints.format, + an array of objects for specifying how a point is normalized and + how it interferes with axis scaling. It accepts the following options: + + ```js + { + x, y: boolean, + number: boolean, + required: boolean, + defaultValue: value, + autoscale: boolean + } + ``` + + "x" and "y" specify whether the value is plotted against the x or y axis, + and is currently used only to calculate axis min-max ranges. The default + format array, for example, looks like this: + + ```js + [ + { x: true, number: true, required: true }, + { y: true, number: true, required: true } + ] + ``` + + This indicates that a point, i.e. [0, 25], consists of two values, with the + first being plotted on the x axis and the second on the y axis. + + If "number" is true, then the value must be numeric, and is set to null if + it cannot be converted to a number. + + "defaultValue" provides a fallback in case the original value is null. This + is for instance handy for bars, where one can omit the third coordinate + (the bottom of the bar), which then defaults to zero. + + If "required" is true, then the value must exist (be non-null) for the + point as a whole to be valid. If no value is provided, then the entire + point is cleared out with nulls, turning it into a gap in the series. + + "autoscale" determines whether the value is considered when calculating an + automatic min-max range for the axes that the value is plotted against. + + - processDatapoints [phase 3] + + ```function(plot, series, datapoints)``` + + Called after normalization of the given series but before finding + min/max of the data points. This hook is useful for implementing data + transformations. "datapoints" contains the normalized data points in + a flat array as datapoints.points with the size of a single point + given in datapoints.pointsize. Here's a simple transform that + multiplies all y coordinates by 2: + + ```js + function multiply(plot, series, datapoints) { + var points = datapoints.points, ps = datapoints.pointsize; + for (var i = 0; i < points.length; i += ps) + points[i + 1] *= 2; + } + ``` + + Note that you must leave datapoints in a good condition as Flot + doesn't check it or do any normalization on it afterwards. + + - processOffset [phase 4] + + ```function(plot, offset)``` + + Called after Flot has initialized the plot's offset, but before it + draws any axes or plot elements. This hook is useful for customizing + the margins between the grid and the edge of the canvas. "offset" is + an object with attributes "top", "bottom", "left" and "right", + corresponding to the margins on the four sides of the plot. + + - drawBackground [phase 5] + + ```function(plot, canvascontext)``` + + Called before all other drawing operations. Used to draw backgrounds + or other custom elements before the plot or axes have been drawn. + + - drawSeries [phase 5] + + ```function(plot, canvascontext, series)``` + + Hook for custom drawing of a single series. Called just before the + standard drawing routine has been called in the loop that draws + each series. + + - draw [phase 5] + + ```function(plot, canvascontext)``` + + Hook for drawing on the canvas. Called after the grid is drawn + (unless it's disabled or grid.aboveData is set) and the series have + been plotted (in case any points, lines or bars have been turned + on). For examples of how to draw things, look at the source code. + + - bindEvents [phase 6] + + ```function(plot, eventHolder)``` + + Called after Flot has setup its event handlers. Should set any + necessary event handlers on eventHolder, a jQuery object with the + canvas, e.g. + + ```js + function (plot, eventHolder) { + eventHolder.mousedown(function (e) { + alert("You pressed the mouse at " + e.pageX + " " + e.pageY); + }); + } + ``` + + Interesting events include click, mousemove, mouseup/down. You can + use all jQuery events. Usually, the event handlers will update the + state by drawing something (add a drawOverlay hook and call + triggerRedrawOverlay) or firing an externally visible event for + user code. See the crosshair plugin for an example. + + Currently, eventHolder actually contains both the static canvas + used for the plot itself and the overlay canvas used for + interactive features because some versions of IE get the stacking + order wrong. The hook only gets one event, though (either for the + overlay or for the static canvas). + + Note that custom plot events generated by Flot are not generated on + eventHolder, but on the div placeholder supplied as the first + argument to the plot call. You can get that with + plot.getPlaceholder() - that's probably also the one you should use + if you need to fire a custom event. + + - drawOverlay [phase 7] + + ```function (plot, canvascontext)``` + + The drawOverlay hook is used for interactive things that need a + canvas to draw on. The model currently used by Flot works the way + that an extra overlay canvas is positioned on top of the static + canvas. This overlay is cleared and then completely redrawn + whenever something interesting happens. This hook is called when + the overlay canvas is to be redrawn. + + "canvascontext" is the 2D context of the overlay canvas. You can + use this to draw things. You'll most likely need some of the + metrics computed by Flot, e.g. plot.width()/plot.height(). See the + crosshair plugin for an example. + + - shutdown [phase 8] + + ```function (plot, eventHolder)``` + + Run when plot.shutdown() is called, which usually only happens in + case a plot is overwritten by a new plot. If you're writing a + plugin that adds extra DOM elements or event handlers, you should + add a callback to clean up after you. Take a look at the section in + the [PLUGINS](PLUGINS.md) document for more info. + + +## Plugins ## + +Plugins extend the functionality of Flot. To use a plugin, simply +include its JavaScript file after Flot in the HTML page. + +If you're worried about download size/latency, you can concatenate all +the plugins you use, and Flot itself for that matter, into one big file +(make sure you get the order right), then optionally run it through a +JavaScript minifier such as YUI Compressor. + +Here's a brief explanation of how the plugin plumbings work: + +Each plugin registers itself in the global array $.plot.plugins. When +you make a new plot object with $.plot, Flot goes through this array +calling the "init" function of each plugin and merging default options +from the "option" attribute of the plugin. The init function gets a +reference to the plot object created and uses this to register hooks +and add new public methods if needed. + +See the [PLUGINS](PLUGINS.md) document for details on how to write a plugin. As the +above description hints, it's actually pretty easy. + + +## Version number ## + +The version number of Flot is available in ```$.plot.version```. diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js new file mode 100644 index 0000000000000..613939256cfc9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js @@ -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. + */ + +/* @notice + * + * This product includes code that is based on flot-charts, which was available + * under a "MIT" license. + * + * The MIT License (MIT) + * + * Copyright (c) 2007-2014 IOLA and Ole Laursen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import $ from 'jquery'; +if (window) window.jQuery = $; +require('./jquery.flot'); +require('./jquery.flot.time'); +require('./jquery.flot.canvas'); +require('./jquery.flot.symbol'); +require('./jquery.flot.crosshair'); +require('./jquery.flot.selection'); +require('./jquery.flot.pie'); +require('./jquery.flot.stack'); +require('./jquery.flot.threshold'); +require('./jquery.flot.fillbetween'); +require('./jquery.flot.log'); +module.exports = $; diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.colorhelpers.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.colorhelpers.js new file mode 100644 index 0000000000000..b2f6dc4e433a3 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.colorhelpers.js @@ -0,0 +1,180 @@ +/* Plugin for jQuery for working with colors. + * + * Version 1.1. + * + * Inspiration from jQuery color animation plugin by John Resig. + * + * Released under the MIT license by Ole Laursen, October 2009. + * + * Examples: + * + * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() + * var c = $.color.extract($("#mydiv"), 'background-color'); + * console.log(c.r, c.g, c.b, c.a); + * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" + * + * Note that .scale() and .add() return the same modified object + * instead of making a new one. + * + * V. 1.1: Fix error handling so e.g. parsing an empty string does + * produce a color rather than just crashing. + */ + +(function($) { + $.color = {}; + + // construct color object with some convenient chainable helpers + $.color.make = function (r, g, b, a) { + var o = {}; + o.r = r || 0; + o.g = g || 0; + o.b = b || 0; + o.a = a != null ? a : 1; + + o.add = function (c, d) { + for (var i = 0; i < c.length; ++i) + o[c.charAt(i)] += d; + return o.normalize(); + }; + + o.scale = function (c, f) { + for (var i = 0; i < c.length; ++i) + o[c.charAt(i)] *= f; + return o.normalize(); + }; + + o.toString = function () { + if (o.a >= 1.0) { + return "rgb("+[o.r, o.g, o.b].join(",")+")"; + } else { + return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")"; + } + }; + + o.normalize = function () { + function clamp(min, value, max) { + return value < min ? min: (value > max ? max: value); + } + + o.r = clamp(0, parseInt(o.r), 255); + o.g = clamp(0, parseInt(o.g), 255); + o.b = clamp(0, parseInt(o.b), 255); + o.a = clamp(0, o.a, 1); + return o; + }; + + o.clone = function () { + return $.color.make(o.r, o.b, o.g, o.a); + }; + + return o.normalize(); + } + + // extract CSS color property from element, going up in the DOM + // if it's "transparent" + $.color.extract = function (elem, css) { + var c; + + do { + c = elem.css(css).toLowerCase(); + // keep going until we find an element that has color, or + // we hit the body or root (have no parent) + if (c != '' && c != 'transparent') + break; + elem = elem.parent(); + } while (elem.length && !$.nodeName(elem.get(0), "body")); + + // catch Safari's way of signalling transparent + if (c == "rgba(0, 0, 0, 0)") + c = "transparent"; + + return $.color.parse(c); + } + + // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"), + // returns color object, if parsing failed, you get black (0, 0, + // 0) out + $.color.parse = function (str) { + var res, m = $.color.make; + + // Look for rgb(num,num,num) + if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)) + return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10)); + + // Look for rgba(num,num,num,num) + if (res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) + return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4])); + + // Look for rgb(num%,num%,num%) + if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)) + return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55); + + // Look for rgba(num%,num%,num%,num) + if (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) + return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4])); + + // Look for #a0b1c2 + if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)) + return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)); + + // Look for #fff + if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)) + return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16)); + + // Otherwise, we're most likely dealing with a named color + var name = $.trim(str).toLowerCase(); + if (name == "transparent") + return m(255, 255, 255, 0); + else { + // default to black + res = lookupColors[name] || [0, 0, 0]; + return m(res[0], res[1], res[2]); + } + } + + var lookupColors = { + aqua:[0,255,255], + azure:[240,255,255], + beige:[245,245,220], + black:[0,0,0], + blue:[0,0,255], + brown:[165,42,42], + cyan:[0,255,255], + darkblue:[0,0,139], + darkcyan:[0,139,139], + darkgrey:[169,169,169], + darkgreen:[0,100,0], + darkkhaki:[189,183,107], + darkmagenta:[139,0,139], + darkolivegreen:[85,107,47], + darkorange:[255,140,0], + darkorchid:[153,50,204], + darkred:[139,0,0], + darksalmon:[233,150,122], + darkviolet:[148,0,211], + fuchsia:[255,0,255], + gold:[255,215,0], + green:[0,128,0], + indigo:[75,0,130], + khaki:[240,230,140], + lightblue:[173,216,230], + lightcyan:[224,255,255], + lightgreen:[144,238,144], + lightgrey:[211,211,211], + lightpink:[255,182,193], + lightyellow:[255,255,224], + lime:[0,255,0], + magenta:[255,0,255], + maroon:[128,0,0], + navy:[0,0,128], + olive:[128,128,0], + orange:[255,165,0], + pink:[255,192,203], + purple:[128,0,128], + violet:[128,0,128], + red:[255,0,0], + silver:[192,192,192], + white:[255,255,255], + yellow:[255,255,0] + }; +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.canvas.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.canvas.js new file mode 100644 index 0000000000000..29328d5812127 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.canvas.js @@ -0,0 +1,345 @@ +/* Flot plugin for drawing all elements of a plot on the canvas. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Flot normally produces certain elements, like axis labels and the legend, using +HTML elements. This permits greater interactivity and customization, and often +looks better, due to cross-browser canvas text inconsistencies and limitations. + +It can also be desirable to render the plot entirely in canvas, particularly +if the goal is to save it as an image, or if Flot is being used in a context +where the HTML DOM does not exist, as is the case within Node.js. This plugin +switches out Flot's standard drawing operations for canvas-only replacements. + +Currently the plugin supports only axis labels, but it will eventually allow +every element of the plot to be rendered directly to canvas. + +The plugin supports these options: + +{ + canvas: boolean +} + +The "canvas" option controls whether full canvas drawing is enabled, making it +possible to toggle on and off. This is useful when a plot uses HTML text in the +browser, but needs to redraw with canvas text when exporting as an image. + +*/ + +(function($) { + + var options = { + canvas: true + }; + + var render, getTextInfo, addText; + + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + + function init(plot, classes) { + + var Canvas = classes.Canvas; + + // We only want to replace the functions once; the second time around + // we would just get our new function back. This whole replacing of + // prototype functions is a disaster, and needs to be changed ASAP. + + if (render == null) { + getTextInfo = Canvas.prototype.getTextInfo, + addText = Canvas.prototype.addText, + render = Canvas.prototype.render; + } + + // Finishes rendering the canvas, including overlaid text + + Canvas.prototype.render = function() { + + if (!plot.getOptions().canvas) { + return render.call(this); + } + + var context = this.context, + cache = this._textCache; + + // For each text layer, render elements marked as active + + context.save(); + context.textBaseline = "middle"; + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + var layerCache = cache[layerKey]; + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey], + updateStyles = true; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + + var info = styleCache[key], + positions = info.positions, + lines = info.lines; + + // Since every element at this level of the cache have the + // same font and fill styles, we can just change them once + // using the values from the first element. + + if (updateStyles) { + context.fillStyle = info.font.color; + context.font = info.font.definition; + updateStyles = false; + } + + for (var i = 0, position; position = positions[i]; i++) { + if (position.active) { + for (var j = 0, line; line = position.lines[j]; j++) { + context.fillText(lines[j].text, line[0], line[1]); + } + } else { + positions.splice(i--, 1); + } + } + + if (positions.length == 0) { + delete styleCache[key]; + } + } + } + } + } + } + } + + context.restore(); + }; + + // Creates (if necessary) and returns a text info object. + // + // When the canvas option is set, the object looks like this: + // + // { + // width: Width of the text's bounding box. + // height: Height of the text's bounding box. + // positions: Array of positions at which this text is drawn. + // lines: [{ + // height: Height of this line. + // widths: Width of this line. + // text: Text on this line. + // }], + // font: { + // definition: Canvas font property string. + // color: Color of the text. + // }, + // } + // + // The positions array contains objects that look like this: + // + // { + // active: Flag indicating whether the text should be visible. + // lines: Array of [x, y] coordinates at which to draw the line. + // x: X coordinate at which to draw the text. + // y: Y coordinate at which to draw the text. + // } + + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + + if (!plot.getOptions().canvas) { + return getTextInfo.call(this, layer, text, font, angle, width); + } + + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number + + text = "" + text; + + // If the font is a font-spec object, generate a CSS definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + if (info == null) { + + var context = this.context; + + // If the font was provided as CSS, create a div with those + // classes and examine it to generate a canvas font spec. + + if (typeof font !== "object") { + + var element = $("
 
") + .css("position", "absolute") + .addClass(typeof font === "string" ? font : null) + .appendTo(this.getTextLayer(layer)); + + font = { + lineHeight: element.height(), + style: element.css("font-style"), + variant: element.css("font-variant"), + weight: element.css("font-weight"), + family: element.css("font-family"), + color: element.css("color") + }; + + // Setting line-height to 1, without units, sets it equal + // to the font-size, even if the font-size is abstract, + // like 'smaller'. This enables us to read the real size + // via the element's height, working around browsers that + // return the literal 'smaller' value. + + font.size = element.css("line-height", 1).height(); + + element.remove(); + } + + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; + + // Create a new info object, initializing the dimensions to + // zero so we can count them up line-by-line. + + info = styleCache[text] = { + width: 0, + height: 0, + positions: [], + lines: [], + font: { + definition: textStyle, + color: font.color + } + }; + + context.save(); + context.font = textStyle; + + // Canvas can't handle multi-line strings; break on various + // newlines, including HTML brs, to build a list of lines. + // Note that we could split directly on regexps, but IE < 9 is + // broken; revisit when we drop IE 7/8 support. + + var lines = (text + "").replace(/
|\r\n|\r/g, "\n").split("\n"); + + for (var i = 0; i < lines.length; ++i) { + + var lineText = lines[i], + measured = context.measureText(lineText); + + info.width = Math.max(measured.width, info.width); + info.height += font.lineHeight; + + info.lines.push({ + text: lineText, + width: measured.width, + height: font.lineHeight + }); + } + + context.restore(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { + + if (!plot.getOptions().canvas) { + return addText.call(this, layer, x, y, text, font, angle, width, halign, valign); + } + + var info = this.getTextInfo(layer, text, font, angle, width), + positions = info.positions, + lines = info.lines; + + // Text is drawn with baseline 'middle', which we need to account + // for by adding half a line's height to the y position. + + y += info.height / lines.length / 2; + + // Tweak the initial y-position to match vertical alignment + + if (valign == "middle") { + y = Math.round(y - info.height / 2); + } else if (valign == "bottom") { + y = Math.round(y - info.height); + } else { + y = Math.round(y); + } + + // FIXME: LEGACY BROWSER FIX + // AFFECTS: Opera < 12.00 + + // Offset the y coordinate, since Opera is off pretty + // consistently compared to the other browsers. + + if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { + y -= 2; + } + + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. + + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = true; + return; + } + } + + // If the text doesn't exist at this position, create a new entry + + position = { + active: true, + lines: [], + x: x, + y: y + }; + + positions.push(position); + + // Fill in the x & y positions of each line, adjusting them + // individually for horizontal alignment. + + for (var i = 0, line; line = lines[i]; i++) { + if (halign == "center") { + position.lines.push([Math.round(x - line.width / 2), y]); + } else if (halign == "right") { + position.lines.push([Math.round(x - line.width), y]); + } else { + position.lines.push([Math.round(x), y]); + } + y += line.height; + } + }; + } + + $.plot.plugins.push({ + init: init, + options: options, + name: "canvas", + version: "1.0" + }); + +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.categories.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.categories.js new file mode 100644 index 0000000000000..2f9b257971499 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.categories.js @@ -0,0 +1,190 @@ +/* Flot plugin for plotting textual data or categories. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Consider a dataset like [["February", 34], ["March", 20], ...]. This plugin +allows you to plot such a dataset directly. + +To enable it, you must specify mode: "categories" on the axis with the textual +labels, e.g. + + $.plot("#placeholder", data, { xaxis: { mode: "categories" } }); + +By default, the labels are ordered as they are met in the data series. If you +need a different ordering, you can specify "categories" on the axis options +and list the categories there: + + xaxis: { + mode: "categories", + categories: ["February", "March", "April"] + } + +If you need to customize the distances between the categories, you can specify +"categories" as an object mapping labels to values + + xaxis: { + mode: "categories", + categories: { "February": 1, "March": 3, "April": 4 } + } + +If you don't specify all categories, the remaining categories will be numbered +from the max value plus 1 (with a spacing of 1 between each). + +Internally, the plugin works by transforming the input data through an auto- +generated mapping where the first category becomes 0, the second 1, etc. +Hence, a point like ["February", 34] becomes [0, 34] internally in Flot (this +is visible in hover and click events that return numbers rather than the +category labels). The plugin also overrides the tick generator to spit out the +categories as ticks instead of the values. + +If you need to map a value back to its label, the mapping is always accessible +as "categories" on the axis object, e.g. plot.getAxes().xaxis.categories. + +*/ + +(function ($) { + var options = { + xaxis: { + categories: null + }, + yaxis: { + categories: null + } + }; + + function processRawData(plot, series, data, datapoints) { + // if categories are enabled, we need to disable + // auto-transformation to numbers so the strings are intact + // for later processing + + var xCategories = series.xaxis.options.mode == "categories", + yCategories = series.yaxis.options.mode == "categories"; + + if (!(xCategories || yCategories)) + return; + + var format = datapoints.format; + + if (!format) { + // FIXME: auto-detection should really not be defined here + var s = series; + format = []; + format.push({ x: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + + if (s.bars.show || (s.lines.show && s.lines.fill)) { + var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); + format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } + + datapoints.format = format; + } + + for (var m = 0; m < format.length; ++m) { + if (format[m].x && xCategories) + format[m].number = false; + + if (format[m].y && yCategories) + format[m].number = false; + } + } + + function getNextIndex(categories) { + var index = -1; + + for (var v in categories) + if (categories[v] > index) + index = categories[v]; + + return index + 1; + } + + function categoriesTickGenerator(axis) { + var res = []; + for (var label in axis.categories) { + var v = axis.categories[label]; + if (v >= axis.min && v <= axis.max) + res.push([v, label]); + } + + res.sort(function (a, b) { return a[0] - b[0]; }); + + return res; + } + + function setupCategoriesForAxis(series, axis, datapoints) { + if (series[axis].options.mode != "categories") + return; + + if (!series[axis].categories) { + // parse options + var c = {}, o = series[axis].options.categories || {}; + if ($.isArray(o)) { + for (var i = 0; i < o.length; ++i) + c[o[i]] = i; + } + else { + for (var v in o) + c[v] = o[v]; + } + + series[axis].categories = c; + } + + // fix ticks + if (!series[axis].options.ticks) + series[axis].options.ticks = categoriesTickGenerator; + + transformPointsOnAxis(datapoints, axis, series[axis].categories); + } + + function transformPointsOnAxis(datapoints, axis, categories) { + // go through the points, transforming them + var points = datapoints.points, + ps = datapoints.pointsize, + format = datapoints.format, + formatColumn = axis.charAt(0), + index = getNextIndex(categories); + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) + continue; + + for (var m = 0; m < ps; ++m) { + var val = points[i + m]; + + if (val == null || !format[m][formatColumn]) + continue; + + if (!(val in categories)) { + categories[val] = index; + ++index; + } + + points[i + m] = categories[val]; + } + } + } + + function processDatapoints(plot, series, datapoints) { + setupCategoriesForAxis(series, "xaxis", datapoints); + setupCategoriesForAxis(series, "yaxis", datapoints); + } + + function init(plot) { + plot.hooks.processRawData.push(processRawData); + plot.hooks.processDatapoints.push(processDatapoints); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'categories', + version: '1.0' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.crosshair.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.crosshair.js new file mode 100644 index 0000000000000..5111695e3d12c --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.crosshair.js @@ -0,0 +1,176 @@ +/* Flot plugin for showing crosshairs when the mouse hovers over the plot. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin supports these options: + + crosshair: { + mode: null or "x" or "y" or "xy" + color: color + lineWidth: number + } + +Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical +crosshair that lets you trace the values on the x axis, "y" enables a +horizontal crosshair and "xy" enables them both. "color" is the color of the +crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of +the drawn lines (default is 1). + +The plugin also adds four public methods: + + - setCrosshair( pos ) + + Set the position of the crosshair. Note that this is cleared if the user + moves the mouse. "pos" is in coordinates of the plot and should be on the + form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple + axes), which is coincidentally the same format as what you get from a + "plothover" event. If "pos" is null, the crosshair is cleared. + + - clearCrosshair() + + Clear the crosshair. + + - lockCrosshair(pos) + + Cause the crosshair to lock to the current location, no longer updating if + the user moves the mouse. Optionally supply a position (passed on to + setCrosshair()) to move it to. + + Example usage: + + var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; + $("#graph").bind( "plothover", function ( evt, position, item ) { + if ( item ) { + // Lock the crosshair to the data point being hovered + myFlot.lockCrosshair({ + x: item.datapoint[ 0 ], + y: item.datapoint[ 1 ] + }); + } else { + // Return normal crosshair operation + myFlot.unlockCrosshair(); + } + }); + + - unlockCrosshair() + + Free the crosshair to move again after locking it. +*/ + +(function ($) { + var options = { + crosshair: { + mode: null, // one of null, "x", "y" or "xy", + color: "rgba(170, 0, 0, 0.80)", + lineWidth: 1 + } + }; + + function init(plot) { + // position of crosshair in pixels + var crosshair = { x: -1, y: -1, locked: false }; + + plot.setCrosshair = function setCrosshair(pos) { + if (!pos) + crosshair.x = -1; + else { + var o = plot.p2c(pos); + crosshair.x = Math.max(0, Math.min(o.left, plot.width())); + crosshair.y = Math.max(0, Math.min(o.top, plot.height())); + } + + plot.triggerRedrawOverlay(); + }; + + plot.clearCrosshair = plot.setCrosshair; // passes null for pos + + plot.lockCrosshair = function lockCrosshair(pos) { + if (pos) + plot.setCrosshair(pos); + crosshair.locked = true; + }; + + plot.unlockCrosshair = function unlockCrosshair() { + crosshair.locked = false; + }; + + function onMouseOut(e) { + if (crosshair.locked) + return; + + if (crosshair.x != -1) { + crosshair.x = -1; + plot.triggerRedrawOverlay(); + } + } + + function onMouseMove(e) { + if (crosshair.locked) + return; + + if (plot.getSelection && plot.getSelection()) { + crosshair.x = -1; // hide the crosshair while selecting + return; + } + + var offset = plot.offset(); + crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); + crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); + plot.triggerRedrawOverlay(); + } + + plot.hooks.bindEvents.push(function (plot, eventHolder) { + if (!plot.getOptions().crosshair.mode) + return; + + eventHolder.mouseout(onMouseOut); + eventHolder.mousemove(onMouseMove); + }); + + plot.hooks.drawOverlay.push(function (plot, ctx) { + var c = plot.getOptions().crosshair; + if (!c.mode) + return; + + var plotOffset = plot.getPlotOffset(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + if (crosshair.x != -1) { + var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; + + ctx.strokeStyle = c.color; + ctx.lineWidth = c.lineWidth; + ctx.lineJoin = "round"; + + ctx.beginPath(); + if (c.mode.indexOf("x") != -1) { + var drawX = Math.floor(crosshair.x) + adj; + ctx.moveTo(drawX, 0); + ctx.lineTo(drawX, plot.height()); + } + if (c.mode.indexOf("y") != -1) { + var drawY = Math.floor(crosshair.y) + adj; + ctx.moveTo(0, drawY); + ctx.lineTo(plot.width(), drawY); + } + ctx.stroke(); + } + ctx.restore(); + }); + + plot.hooks.shutdown.push(function (plot, eventHolder) { + eventHolder.unbind("mouseout", onMouseOut); + eventHolder.unbind("mousemove", onMouseMove); + }); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'crosshair', + version: '1.0' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.errorbars.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.errorbars.js new file mode 100644 index 0000000000000..655036e0db846 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.errorbars.js @@ -0,0 +1,353 @@ +/* Flot plugin for plotting error bars. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Error bars are used to show standard deviation and other statistical +properties in a plot. + +* Created by Rui Pereira - rui (dot) pereira (at) gmail (dot) com + +This plugin allows you to plot error-bars over points. Set "errorbars" inside +the points series to the axis name over which there will be error values in +your data array (*even* if you do not intend to plot them later, by setting +"show: null" on xerr/yerr). + +The plugin supports these options: + + series: { + points: { + errorbars: "x" or "y" or "xy", + xerr: { + show: null/false or true, + asymmetric: null/false or true, + upperCap: null or "-" or function, + lowerCap: null or "-" or function, + color: null or color, + radius: null or number + }, + yerr: { same options as xerr } + } + } + +Each data point array is expected to be of the type: + + "x" [ x, y, xerr ] + "y" [ x, y, yerr ] + "xy" [ x, y, xerr, yerr ] + +Where xerr becomes xerr_lower,xerr_upper for the asymmetric error case, and +equivalently for yerr. E.g., a datapoint for the "xy" case with symmetric +error-bars on X and asymmetric on Y would be: + + [ x, y, xerr, yerr_lower, yerr_upper ] + +By default no end caps are drawn. Setting upperCap and/or lowerCap to "-" will +draw a small cap perpendicular to the error bar. They can also be set to a +user-defined drawing function, with (ctx, x, y, radius) as parameters, as e.g.: + + function drawSemiCircle( ctx, x, y, radius ) { + ctx.beginPath(); + ctx.arc( x, y, radius, 0, Math.PI, false ); + ctx.moveTo( x - radius, y ); + ctx.lineTo( x + radius, y ); + ctx.stroke(); + } + +Color and radius both default to the same ones of the points series if not +set. The independent radius parameter on xerr/yerr is useful for the case when +we may want to add error-bars to a line, without showing the interconnecting +points (with radius: 0), and still showing end caps on the error-bars. +shadowSize and lineWidth are derived as well from the points series. + +*/ + +(function ($) { + var options = { + series: { + points: { + errorbars: null, //should be 'x', 'y' or 'xy' + xerr: { err: 'x', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null}, + yerr: { err: 'y', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null} + } + } + }; + + function processRawData(plot, series, data, datapoints){ + if (!series.points.errorbars) + return; + + // x,y values + var format = [ + { x: true, number: true, required: true }, + { y: true, number: true, required: true } + ]; + + var errors = series.points.errorbars; + // error bars - first X then Y + if (errors == 'x' || errors == 'xy') { + // lower / upper error + if (series.points.xerr.asymmetric) { + format.push({ x: true, number: true, required: true }); + format.push({ x: true, number: true, required: true }); + } else + format.push({ x: true, number: true, required: true }); + } + if (errors == 'y' || errors == 'xy') { + // lower / upper error + if (series.points.yerr.asymmetric) { + format.push({ y: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + } else + format.push({ y: true, number: true, required: true }); + } + datapoints.format = format; + } + + function parseErrors(series, i){ + + var points = series.datapoints.points; + + // read errors from points array + var exl = null, + exu = null, + eyl = null, + eyu = null; + var xerr = series.points.xerr, + yerr = series.points.yerr; + + var eb = series.points.errorbars; + // error bars - first X + if (eb == 'x' || eb == 'xy') { + if (xerr.asymmetric) { + exl = points[i + 2]; + exu = points[i + 3]; + if (eb == 'xy') + if (yerr.asymmetric){ + eyl = points[i + 4]; + eyu = points[i + 5]; + } else eyl = points[i + 4]; + } else { + exl = points[i + 2]; + if (eb == 'xy') + if (yerr.asymmetric) { + eyl = points[i + 3]; + eyu = points[i + 4]; + } else eyl = points[i + 3]; + } + // only Y + } else if (eb == 'y') + if (yerr.asymmetric) { + eyl = points[i + 2]; + eyu = points[i + 3]; + } else eyl = points[i + 2]; + + // symmetric errors? + if (exu == null) exu = exl; + if (eyu == null) eyu = eyl; + + var errRanges = [exl, exu, eyl, eyu]; + // nullify if not showing + if (!xerr.show){ + errRanges[0] = null; + errRanges[1] = null; + } + if (!yerr.show){ + errRanges[2] = null; + errRanges[3] = null; + } + return errRanges; + } + + function drawSeriesErrors(plot, ctx, s){ + + var points = s.datapoints.points, + ps = s.datapoints.pointsize, + ax = [s.xaxis, s.yaxis], + radius = s.points.radius, + err = [s.points.xerr, s.points.yerr]; + + //sanity check, in case some inverted axis hack is applied to flot + var invertX = false; + if (ax[0].p2c(ax[0].max) < ax[0].p2c(ax[0].min)) { + invertX = true; + var tmp = err[0].lowerCap; + err[0].lowerCap = err[0].upperCap; + err[0].upperCap = tmp; + } + + var invertY = false; + if (ax[1].p2c(ax[1].min) < ax[1].p2c(ax[1].max)) { + invertY = true; + var tmp = err[1].lowerCap; + err[1].lowerCap = err[1].upperCap; + err[1].upperCap = tmp; + } + + for (var i = 0; i < s.datapoints.points.length; i += ps) { + + //parse + var errRanges = parseErrors(s, i); + + //cycle xerr & yerr + for (var e = 0; e < err.length; e++){ + + var minmax = [ax[e].min, ax[e].max]; + + //draw this error? + if (errRanges[e * err.length]){ + + //data coordinates + var x = points[i], + y = points[i + 1]; + + //errorbar ranges + var upper = [x, y][e] + errRanges[e * err.length + 1], + lower = [x, y][e] - errRanges[e * err.length]; + + //points outside of the canvas + if (err[e].err == 'x') + if (y > ax[1].max || y < ax[1].min || upper < ax[0].min || lower > ax[0].max) + continue; + if (err[e].err == 'y') + if (x > ax[0].max || x < ax[0].min || upper < ax[1].min || lower > ax[1].max) + continue; + + // prevent errorbars getting out of the canvas + var drawUpper = true, + drawLower = true; + + if (upper > minmax[1]) { + drawUpper = false; + upper = minmax[1]; + } + if (lower < minmax[0]) { + drawLower = false; + lower = minmax[0]; + } + + //sanity check, in case some inverted axis hack is applied to flot + if ((err[e].err == 'x' && invertX) || (err[e].err == 'y' && invertY)) { + //swap coordinates + var tmp = lower; + lower = upper; + upper = tmp; + tmp = drawLower; + drawLower = drawUpper; + drawUpper = tmp; + tmp = minmax[0]; + minmax[0] = minmax[1]; + minmax[1] = tmp; + } + + // convert to pixels + x = ax[0].p2c(x), + y = ax[1].p2c(y), + upper = ax[e].p2c(upper); + lower = ax[e].p2c(lower); + minmax[0] = ax[e].p2c(minmax[0]); + minmax[1] = ax[e].p2c(minmax[1]); + + //same style as points by default + var lw = err[e].lineWidth ? err[e].lineWidth : s.points.lineWidth, + sw = s.points.shadowSize != null ? s.points.shadowSize : s.shadowSize; + + //shadow as for points + if (lw > 0 && sw > 0) { + var w = sw / 2; + ctx.lineWidth = w; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w + w/2, minmax); + + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w/2, minmax); + } + + ctx.strokeStyle = err[e].color? err[e].color: s.color; + ctx.lineWidth = lw; + //draw it + drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, 0, minmax); + } + } + } + } + + function drawError(ctx,err,x,y,upper,lower,drawUpper,drawLower,radius,offset,minmax){ + + //shadow offset + y += offset; + upper += offset; + lower += offset; + + // error bar - avoid plotting over circles + if (err.err == 'x'){ + if (upper > x + radius) drawPath(ctx, [[upper,y],[Math.max(x + radius,minmax[0]),y]]); + else drawUpper = false; + if (lower < x - radius) drawPath(ctx, [[Math.min(x - radius,minmax[1]),y],[lower,y]] ); + else drawLower = false; + } + else { + if (upper < y - radius) drawPath(ctx, [[x,upper],[x,Math.min(y - radius,minmax[0])]] ); + else drawUpper = false; + if (lower > y + radius) drawPath(ctx, [[x,Math.max(y + radius,minmax[1])],[x,lower]] ); + else drawLower = false; + } + + //internal radius value in errorbar, allows to plot radius 0 points and still keep proper sized caps + //this is a way to get errorbars on lines without visible connecting dots + radius = err.radius != null? err.radius: radius; + + // upper cap + if (drawUpper) { + if (err.upperCap == '-'){ + if (err.err=='x') drawPath(ctx, [[upper,y - radius],[upper,y + radius]] ); + else drawPath(ctx, [[x - radius,upper],[x + radius,upper]] ); + } else if ($.isFunction(err.upperCap)){ + if (err.err=='x') err.upperCap(ctx, upper, y, radius); + else err.upperCap(ctx, x, upper, radius); + } + } + // lower cap + if (drawLower) { + if (err.lowerCap == '-'){ + if (err.err=='x') drawPath(ctx, [[lower,y - radius],[lower,y + radius]] ); + else drawPath(ctx, [[x - radius,lower],[x + radius,lower]] ); + } else if ($.isFunction(err.lowerCap)){ + if (err.err=='x') err.lowerCap(ctx, lower, y, radius); + else err.lowerCap(ctx, x, lower, radius); + } + } + } + + function drawPath(ctx, pts){ + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (var p=1; p < pts.length; p++) + ctx.lineTo(pts[p][0], pts[p][1]); + ctx.stroke(); + } + + function draw(plot, ctx){ + var plotOffset = plot.getPlotOffset(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + $.each(plot.getData(), function (i, s) { + if (s.points.errorbars && (s.points.xerr.show || s.points.yerr.show)) + drawSeriesErrors(plot, ctx, s); + }); + ctx.restore(); + } + + function init(plot) { + plot.hooks.processRawData.push(processRawData); + plot.hooks.draw.push(draw); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'errorbars', + version: '1.0' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.fillbetween.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.fillbetween.js new file mode 100644 index 0000000000000..18b15d26db8c9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.fillbetween.js @@ -0,0 +1,226 @@ +/* Flot plugin for computing bottoms for filled line and bar charts. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The case: you've got two series that you want to fill the area between. In Flot +terms, you need to use one as the fill bottom of the other. You can specify the +bottom of each data point as the third coordinate manually, or you can use this +plugin to compute it for you. + +In order to name the other series, you need to give it an id, like this: + + var dataset = [ + { data: [ ... ], id: "foo" } , // use default bottom + { data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom + ]; + + $.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }}); + +As a convenience, if the id given is a number that doesn't appear as an id in +the series, it is interpreted as the index in the array instead (so fillBetween: +0 can also mean the first series). + +Internally, the plugin modifies the datapoints in each series. For line series, +extra data points might be inserted through interpolation. Note that at points +where the bottom line is not defined (due to a null point or start/end of line), +the current line will show a gap too. The algorithm comes from the +jquery.flot.stack.js plugin, possibly some code could be shared. + +*/ + +(function ( $ ) { + + var options = { + series: { + fillBetween: null // or number + } + }; + + function init( plot ) { + + function findBottomSeries( s, allseries ) { + + var i; + + for ( i = 0; i < allseries.length; ++i ) { + if ( allseries[ i ].id === s.fillBetween ) { + return allseries[ i ]; + } + } + + if ( typeof s.fillBetween === "number" ) { + if ( s.fillBetween < 0 || s.fillBetween >= allseries.length ) { + return null; + } + return allseries[ s.fillBetween ]; + } + + return null; + } + + function computeFillBottoms( plot, s, datapoints ) { + + if ( s.fillBetween == null ) { + return; + } + + var other = findBottomSeries( s, plot.getData() ); + + if ( !other ) { + return; + } + + var ps = datapoints.pointsize, + points = datapoints.points, + otherps = other.datapoints.pointsize, + otherpoints = other.datapoints.points, + newpoints = [], + px, py, intery, qx, qy, bottom, + withlines = s.lines.show, + withbottom = ps > 2 && datapoints.format[2].y, + withsteps = withlines && s.lines.steps, + fromgap = true, + i = 0, + j = 0, + l, m; + + while ( true ) { + + if ( i >= points.length ) { + break; + } + + l = newpoints.length; + + if ( points[ i ] == null ) { + + // copy gaps + + for ( m = 0; m < ps; ++m ) { + newpoints.push( points[ i + m ] ); + } + + i += ps; + + } else if ( j >= otherpoints.length ) { + + // for lines, we can't use the rest of the points + + if ( !withlines ) { + for ( m = 0; m < ps; ++m ) { + newpoints.push( points[ i + m ] ); + } + } + + i += ps; + + } else if ( otherpoints[ j ] == null ) { + + // oops, got a gap + + for ( m = 0; m < ps; ++m ) { + newpoints.push( null ); + } + + fromgap = true; + j += otherps; + + } else { + + // cases where we actually got two points + + px = points[ i ]; + py = points[ i + 1 ]; + qx = otherpoints[ j ]; + qy = otherpoints[ j + 1 ]; + bottom = 0; + + if ( px === qx ) { + + for ( m = 0; m < ps; ++m ) { + newpoints.push( points[ i + m ] ); + } + + //newpoints[ l + 1 ] += qy; + bottom = qy; + + i += ps; + j += otherps; + + } else if ( px > qx ) { + + // we got past point below, might need to + // insert interpolated extra point + + if ( withlines && i > 0 && points[ i - ps ] != null ) { + intery = py + ( points[ i - ps + 1 ] - py ) * ( qx - px ) / ( points[ i - ps ] - px ); + newpoints.push( qx ); + newpoints.push( intery ); + for ( m = 2; m < ps; ++m ) { + newpoints.push( points[ i + m ] ); + } + bottom = qy; + } + + j += otherps; + + } else { // px < qx + + // if we come from a gap, we just skip this point + + if ( fromgap && withlines ) { + i += ps; + continue; + } + + for ( m = 0; m < ps; ++m ) { + newpoints.push( points[ i + m ] ); + } + + // we might be able to interpolate a point below, + // this can give us a better y + + if ( withlines && j > 0 && otherpoints[ j - otherps ] != null ) { + bottom = qy + ( otherpoints[ j - otherps + 1 ] - qy ) * ( px - qx ) / ( otherpoints[ j - otherps ] - qx ); + } + + //newpoints[l + 1] += bottom; + + i += ps; + } + + fromgap = false; + + if ( l !== newpoints.length && withbottom ) { + newpoints[ l + 2 ] = bottom; + } + } + + // maintain the line steps invariant + + if ( withsteps && l !== newpoints.length && l > 0 && + newpoints[ l ] !== null && + newpoints[ l ] !== newpoints[ l - ps ] && + newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ] ) { + for (m = 0; m < ps; ++m) { + newpoints[ l + ps + m ] = newpoints[ l + m ]; + } + newpoints[ l + 1 ] = newpoints[ l - ps + 1 ]; + } + } + + datapoints.points = newpoints; + } + + plot.hooks.processDatapoints.push( computeFillBottoms ); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: "fillbetween", + version: "1.0" + }); + +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.image.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.image.js new file mode 100644 index 0000000000000..178f0e69069ef --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.image.js @@ -0,0 +1,241 @@ +/* Flot plugin for plotting images. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The data syntax is [ [ image, x1, y1, x2, y2 ], ... ] where (x1, y1) and +(x2, y2) are where you intend the two opposite corners of the image to end up +in the plot. Image must be a fully loaded JavaScript image (you can make one +with new Image()). If the image is not complete, it's skipped when plotting. + +There are two helpers included for retrieving images. The easiest work the way +that you put in URLs instead of images in the data, like this: + + [ "myimage.png", 0, 0, 10, 10 ] + +Then call $.plot.image.loadData( data, options, callback ) where data and +options are the same as you pass in to $.plot. This loads the images, replaces +the URLs in the data with the corresponding images and calls "callback" when +all images are loaded (or failed loading). In the callback, you can then call +$.plot with the data set. See the included example. + +A more low-level helper, $.plot.image.load(urls, callback) is also included. +Given a list of URLs, it calls callback with an object mapping from URL to +Image object when all images are loaded or have failed loading. + +The plugin supports these options: + + series: { + images: { + show: boolean + anchor: "corner" or "center" + alpha: [ 0, 1 ] + } + } + +They can be specified for a specific series: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + images: { ... } + ]) + +Note that because the data format is different from usual data points, you +can't use images with anything else in a specific data series. + +Setting "anchor" to "center" causes the pixels in the image to be anchored at +the corner pixel centers inside of at the pixel corners, effectively letting +half a pixel stick out to each side in the plot. + +A possible future direction could be support for tiling for large images (like +Google Maps). + +*/ + +(function ($) { + var options = { + series: { + images: { + show: false, + alpha: 1, + anchor: "corner" // or "center" + } + } + }; + + $.plot.image = {}; + + $.plot.image.loadDataImages = function (series, options, callback) { + var urls = [], points = []; + + var defaultShow = options.series.images.show; + + $.each(series, function (i, s) { + if (!(defaultShow || s.images.show)) + return; + + if (s.data) + s = s.data; + + $.each(s, function (i, p) { + if (typeof p[0] == "string") { + urls.push(p[0]); + points.push(p); + } + }); + }); + + $.plot.image.load(urls, function (loadedImages) { + $.each(points, function (i, p) { + var url = p[0]; + if (loadedImages[url]) + p[0] = loadedImages[url]; + }); + + callback(); + }); + } + + $.plot.image.load = function (urls, callback) { + var missing = urls.length, loaded = {}; + if (missing == 0) + callback({}); + + $.each(urls, function (i, url) { + var handler = function () { + --missing; + + loaded[url] = this; + + if (missing == 0) + callback(loaded); + }; + + $('').load(handler).error(handler).attr('src', url); + }); + }; + + function drawSeries(plot, ctx, series) { + var plotOffset = plot.getPlotOffset(); + + if (!series.images || !series.images.show) + return; + + var points = series.datapoints.points, + ps = series.datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + var img = points[i], + x1 = points[i + 1], y1 = points[i + 2], + x2 = points[i + 3], y2 = points[i + 4], + xaxis = series.xaxis, yaxis = series.yaxis, + tmp; + + // actually we should check img.complete, but it + // appears to be a somewhat unreliable indicator in + // IE6 (false even after load event) + if (!img || img.width <= 0 || img.height <= 0) + continue; + + if (x1 > x2) { + tmp = x2; + x2 = x1; + x1 = tmp; + } + if (y1 > y2) { + tmp = y2; + y2 = y1; + y1 = tmp; + } + + // if the anchor is at the center of the pixel, expand the + // image by 1/2 pixel in each direction + if (series.images.anchor == "center") { + tmp = 0.5 * (x2-x1) / (img.width - 1); + x1 -= tmp; + x2 += tmp; + tmp = 0.5 * (y2-y1) / (img.height - 1); + y1 -= tmp; + y2 += tmp; + } + + // clip + if (x1 == x2 || y1 == y2 || + x1 >= xaxis.max || x2 <= xaxis.min || + y1 >= yaxis.max || y2 <= yaxis.min) + continue; + + var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height; + if (x1 < xaxis.min) { + sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1); + x1 = xaxis.min; + } + + if (x2 > xaxis.max) { + sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1); + x2 = xaxis.max; + } + + if (y1 < yaxis.min) { + sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1); + y1 = yaxis.min; + } + + if (y2 > yaxis.max) { + sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1); + y2 = yaxis.max; + } + + x1 = xaxis.p2c(x1); + x2 = xaxis.p2c(x2); + y1 = yaxis.p2c(y1); + y2 = yaxis.p2c(y2); + + // the transformation may have swapped us + if (x1 > x2) { + tmp = x2; + x2 = x1; + x1 = tmp; + } + if (y1 > y2) { + tmp = y2; + y2 = y1; + y1 = tmp; + } + + tmp = ctx.globalAlpha; + ctx.globalAlpha *= series.images.alpha; + ctx.drawImage(img, + sx1, sy1, sx2 - sx1, sy2 - sy1, + x1 + plotOffset.left, y1 + plotOffset.top, + x2 - x1, y2 - y1); + ctx.globalAlpha = tmp; + } + } + + function processRawData(plot, series, data, datapoints) { + if (!series.images.show) + return; + + // format is Image, x1, y1, x2, y2 (opposite corners) + datapoints.format = [ + { required: true }, + { x: true, number: true, required: true }, + { y: true, number: true, required: true }, + { x: true, number: true, required: true }, + { y: true, number: true, required: true } + ]; + } + + function init(plot) { + plot.hooks.processRawData.push(processRawData); + plot.hooks.drawSeries.push(drawSeries); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'image', + version: '1.1' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.js new file mode 100644 index 0000000000000..43db1cc3d93db --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.js @@ -0,0 +1,3168 @@ +/* JavaScript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ + +// first an inline dependency, jquery.colorhelpers.js, we inline it here +// for convenience + +/* Plugin for jQuery for working with colors. + * + * Version 1.1. + * + * Inspiration from jQuery color animation plugin by John Resig. + * + * Released under the MIT license by Ole Laursen, October 2009. + * + * Examples: + * + * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() + * var c = $.color.extract($("#mydiv"), 'background-color'); + * console.log(c.r, c.g, c.b, c.a); + * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" + * + * Note that .scale() and .add() return the same modified object + * instead of making a new one. + * + * V. 1.1: Fix error handling so e.g. parsing an empty string does + * produce a color rather than just crashing. + */ +(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return valuemax?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); + +// the actual Flot code +(function($) { + + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + + // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM + // operation produces the same effect as detach, i.e. removing the element + // without touching its jQuery data. + + // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. + + if (!$.fn.detach) { + $.fn.detach = function() { + return this.each(function() { + if (this.parentNode) { + this.parentNode.removeChild( this ); + } + }); + }; + } + + /////////////////////////////////////////////////////////////////////////// + // The Canvas object is a wrapper around an HTML5 tag. + // + // @constructor + // @param {string} cls List of classes to apply to the canvas. + // @param {element} container Element onto which to append the canvas. + // + // Requiring a container is a little iffy, but unfortunately canvas + // operations don't work unless the canvas is attached to the DOM. + + function Canvas(cls, container) { + + var element = container.children("." + cls)[0]; + + if (element == null) { + + element = document.createElement("canvas"); + element.className = cls; + + $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) + .appendTo(container); + + // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas + + if (!element.getContext) { + if (window.G_vmlCanvasManager) { + element = window.G_vmlCanvasManager.initElement(element); + } else { + throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); + } + } + } + + this.element = element; + + var context = this.context = element.getContext("2d"); + + // Determine the screen's ratio of physical to device-independent + // pixels. This is the ratio between the canvas width that the browser + // advertises and the number of pixels actually present in that space. + + // The iPhone 4, for example, has a device-independent width of 320px, + // but its screen is actually 640px wide. It therefore has a pixel + // ratio of 2, while most normal devices have a ratio of 1. + + var devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = + context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || 1; + + this.pixelRatio = devicePixelRatio / backingStoreRatio; + + // Size the canvas to match the internal dimensions of its container + + this.resize(container.width(), container.height()); + + // Collection of HTML div layers for text overlaid onto the canvas + + this.textContainer = null; + this.text = {}; + + // Cache of text fragments and metrics, so we can avoid expensively + // re-calculating them when the plot is re-rendered in a loop. + + this._textCache = {}; + } + + // Resizes the canvas to the given dimensions. + // + // @param {number} width New width of the canvas, in pixels. + // @param {number} width New height of the canvas, in pixels. + + Canvas.prototype.resize = function(width, height) { + + if (width <= 0 || height <= 0) { + throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); + } + + var element = this.element, + context = this.context, + pixelRatio = this.pixelRatio; + + // Resize the canvas, increasing its density based on the display's + // pixel ratio; basically giving it more pixels without increasing the + // size of its element, to take advantage of the fact that retina + // displays have that many more pixels in the same advertised space. + + // Resizing should reset the state (excanvas seems to be buggy though) + + if (this.width != width) { + element.width = width * pixelRatio; + element.style.width = width + "px"; + this.width = width; + } + + if (this.height != height) { + element.height = height * pixelRatio; + element.style.height = height + "px"; + this.height = height; + } + + // Save the context, so we can reset in case we get replotted. The + // restore ensure that we're really back at the initial state, and + // should be safe even if we haven't saved the initial state yet. + + context.restore(); + context.save(); + + // Scale the coordinate space to match the display density; so even though we + // may have twice as many pixels, we still want lines and other drawing to + // appear at the same size; the extra pixels will just make them crisper. + + context.scale(pixelRatio, pixelRatio); + }; + + // Clears the entire canvas area, not including any overlaid HTML text + + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + }; + + // Finishes rendering the canvas, including managing the text overlay. + + Canvas.prototype.render = function() { + + var cache = this._textCache; + + // For each text layer, add elements marked as active that haven't + // already been rendered, and remove those that are no longer active. + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + + var layer = this.getTextLayer(layerKey), + layerCache = cache[layerKey]; + + layer.hide(); + + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + + var positions = styleCache[key].positions; + + for (var i = 0, position; position = positions[i]; i++) { + if (position.active) { + if (!position.rendered) { + layer.append(position.element); + position.rendered = true; + } + } else { + positions.splice(i--, 1); + if (position.rendered) { + position.element.detach(); + } + } + } + + if (positions.length == 0) { + delete styleCache[key]; + } + } + } + } + } + + layer.show(); + } + } + }; + + // Creates (if necessary) and returns the text overlay container. + // + // @param {string} classes String of space-separated CSS classes used to + // uniquely identify the text layer. + // @return {object} The jQuery-wrapped text-layer div. + + Canvas.prototype.getTextLayer = function(classes) { + + var layer = this.text[classes]; + + // Create the text layer if it doesn't exist + + if (layer == null) { + + // Create the text layer container, if it doesn't exist + + if (this.textContainer == null) { + this.textContainer = $("
") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + 'font-size': "smaller", + color: "#545454" + }) + .insertAfter(this.element); + } + + layer = this.text[classes] = $("
") + .addClass(classes) + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .appendTo(this.textContainer); + } + + return layer; + }; + + // Creates (if necessary) and returns a text info object. + // + // The object looks like this: + // + // { + // width: Width of the text's wrapper div. + // height: Height of the text's wrapper div. + // element: The jQuery-wrapped HTML div containing the text. + // positions: Array of positions at which this text is drawn. + // } + // + // The positions array contains objects that look like this: + // + // { + // active: Flag indicating whether the text should be visible. + // rendered: Flag indicating whether the text is currently visible. + // element: The jQuery-wrapped HTML div containing the text. + // x: X coordinate at which to draw the text. + // y: Y coordinate at which to draw the text. + // } + // + // Each position after the first receives a clone of the original element. + // + // The idea is that that the width, height, and general 'identity' of the + // text is constant no matter where it is placed; the placements are a + // secondary property. + // + // Canvas maintains a cache of recently-used text info objects; getTextInfo + // either returns the cached element or creates a new entry. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {string} text Text string to retrieve info for. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @return {object} a text info object. + + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number or such + + text = "" + text; + + // If the font is a font-spec object, generate a CSS font definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + // If we can't find a matching element in our cache, create a new one + + if (info == null) { + + var element = $("
").html(text) + .css({ + position: "absolute", + 'max-width': width, + top: -9999 + }) + .appendTo(this.getTextLayer(layer)); + + if (typeof font === "object") { + element.css({ + font: textStyle, + color: font.color + }); + } else if (typeof font === "string") { + element.addClass(font); + } + + info = styleCache[text] = { + width: element.outerWidth(true), + height: element.outerHeight(true), + element: element, + positions: [] + }; + + element.detach(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + // + // The text isn't drawn immediately; it is marked as rendering, which will + // result in its addition to the canvas on the next render pass. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number} x X coordinate at which to draw the text. + // @param {number} y Y coordinate at which to draw the text. + // @param {string} text Text string to draw. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @param {string=} halign Horizontal alignment of the text; either "left", + // "center" or "right". + // @param {string=} valign Vertical alignment of the text; either "top", + // "middle" or "bottom". + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { + + var info = this.getTextInfo(layer, text, font, angle, width), + positions = info.positions; + + // Tweak the div's position to match the text's alignment + + if (halign == "center") { + x -= info.width / 2; + } else if (halign == "right") { + x -= info.width; + } + + if (valign == "middle") { + y -= info.height / 2; + } else if (valign == "bottom") { + y -= info.height; + } + + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. + + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = true; + return; + } + } + + // If the text doesn't exist at this position, create a new entry + + // For the very first position we'll re-use the original element, + // while for subsequent ones we'll clone it. + + position = { + active: true, + rendered: false, + element: positions.length ? info.element.clone() : info.element, + x: x, + y: y + }; + + positions.push(position); + + // Move the element to its final position within the container + + position.element.css({ + top: Math.round(y), + left: Math.round(x), + 'text-align': halign // In case the text wraps + }); + }; + + // Removes one or more text strings from the canvas text overlay. + // + // If no parameters are given, all text within the layer is removed. + // + // Note that the text is not immediately removed; it is simply marked as + // inactive, which will result in its removal on the next render pass. + // This avoids the performance penalty for 'clear and redraw' behavior, + // where we potentially get rid of all text on a layer, but will likely + // add back most or all of it later, as when redrawing axes, for example. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number=} x X coordinate of the text. + // @param {number=} y Y coordinate of the text. + // @param {string=} text Text string to remove. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which the text is rotated, in degrees. + // Angle is currently unused, it will be implemented in the future. + + Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { + if (text == null) { + var layerCache = this._textCache[layer]; + if (layerCache != null) { + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + var positions = styleCache[key].positions; + for (var i = 0, position; position = positions[i]; i++) { + position.active = false; + } + } + } + } + } + } + } else { + var positions = this.getTextInfo(layer, text, font, angle).positions; + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = false; + } + } + } + }; + + /////////////////////////////////////////////////////////////////////////// + // The top-level container for the entire plot. + + function Plot(placeholder, data_, options_, plugins) { + // data is on the form: + // [ series1, series2 ... ] + // where series is either just the data as [ [x1, y1], [x2, y2], ... ] + // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } + + var series = [], + options = { + // the color theme used for graphs + colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], + legend: { + show: true, + noColumns: 1, // number of columns in legend table + labelFormatter: null, // fn: string -> string + labelBoxBorderColor: "#ccc", // border color for the little label boxes + container: null, // container (as jQuery object) to put legend in, null means default on top of graph + position: "ne", // position of default legend container within plot + margin: 5, // distance from grid edge to default legend container within plot + backgroundColor: null, // null means auto-detect + backgroundOpacity: 0.85, // set to 0 to avoid background + sorted: null // default to no legend sorting + }, + xaxis: { + show: null, // null = auto-detect, true = always, false = never + position: "bottom", // or "top" + mode: null, // null or "time" + font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } + color: null, // base color, labels, ticks + tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" + transform: null, // null or f: number -> number to transform axis + inverseTransform: null, // if transform is set, this should be the inverse function + min: null, // min. value to show, null means set automatically + max: null, // max. value to show, null means set automatically + autoscaleMargin: null, // margin in % to add if auto-setting min/max + ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks + tickFormatter: null, // fn: number -> string + labelWidth: null, // size of tick labels in pixels + labelHeight: null, + reserveSpace: null, // whether to reserve space even if axis isn't shown + tickLength: null, // size in pixels of ticks, or "full" for whole line + alignTicksWithAxis: null, // axis number or null for no sync + tickDecimals: null, // no. of decimals, null means auto + tickSize: null, // number or [number, "unit"] + minTickSize: null // number or [number, "unit"] + }, + yaxis: { + autoscaleMargin: 0.02, + position: "left" // or "right" + }, + xaxes: [], + yaxes: [], + series: { + points: { + show: false, + radius: 3, + lineWidth: 2, // in pixels + fill: true, + fillColor: "#ffffff", + symbol: "circle" // or callback + }, + lines: { + // we don't put in show: false so we can see + // whether lines were actively disabled + lineWidth: 2, // in pixels + fill: false, + fillColor: null, + steps: false + // Omit 'zero', so we can later default its value to + // match that of the 'fill' option. + }, + bars: { + show: false, + lineWidth: 2, // in pixels + barWidth: 1, // in units of the x axis + fill: true, + fillColor: null, + align: "left", // "left", "right", or "center" + horizontal: false, + zero: true + }, + shadowSize: 3, + highlightColor: null + }, + grid: { + show: true, + aboveData: false, + color: "#545454", // primary color used for outline and labels + backgroundColor: null, // null for transparent, else color + borderColor: null, // set if different from the grid color + tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" + margin: 0, // distance from the canvas edge to the grid + labelMargin: 5, // in pixels + axisMargin: 8, // in pixels + borderWidth: 2, // in pixels + minBorderMargin: null, // in pixels, null means taken from points radius + markings: null, // array of ranges or fn: axes -> array of ranges + markingsColor: "#f4f4f4", + markingsLineWidth: 2, + // interactive stuff + clickable: false, + hoverable: false, + autoHighlight: true, // highlight in case mouse is near + mouseActiveRadius: 10 // how far the mouse can be away to activate an item + }, + interaction: { + redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow + }, + hooks: {} + }, + surface = null, // the canvas for the plot itself + overlay = null, // canvas for interactive stuff on top of plot + eventHolder = null, // jQuery object that events should be bound to + ctx = null, octx = null, + xaxes = [], yaxes = [], + plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, + plotWidth = 0, plotHeight = 0, + hooks = { + processOptions: [], + processRawData: [], + processDatapoints: [], + processOffset: [], + drawBackground: [], + drawSeries: [], + draw: [], + bindEvents: [], + drawOverlay: [], + shutdown: [] + }, + plot = this; + + // public functions + plot.setData = setData; + plot.setupGrid = setupGrid; + plot.draw = draw; + plot.getPlaceholder = function() { return placeholder; }; + plot.getCanvas = function() { return surface.element; }; + plot.getPlotOffset = function() { return plotOffset; }; + plot.width = function () { return plotWidth; }; + plot.height = function () { return plotHeight; }; + plot.offset = function () { + var o = eventHolder.offset(); + o.left += plotOffset.left; + o.top += plotOffset.top; + return o; + }; + plot.getData = function () { return series; }; + plot.getAxes = function () { + var res = {}, i; + $.each(xaxes.concat(yaxes), function (_, axis) { + if (axis) + res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; + }); + return res; + }; + plot.getXAxes = function () { return xaxes; }; + plot.getYAxes = function () { return yaxes; }; + plot.c2p = canvasToAxisCoords; + plot.p2c = axisToCanvasCoords; + plot.getOptions = function () { return options; }; + plot.highlight = highlight; + plot.unhighlight = unhighlight; + plot.triggerRedrawOverlay = triggerRedrawOverlay; + plot.pointOffset = function(point) { + return { + left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), + top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) + }; + }; + plot.shutdown = shutdown; + plot.destroy = function () { + shutdown(); + placeholder.removeData("plot").empty(); + + series = []; + options = null; + surface = null; + overlay = null; + eventHolder = null; + ctx = null; + octx = null; + xaxes = []; + yaxes = []; + hooks = null; + highlights = []; + plot = null; + }; + plot.resize = function () { + var width = placeholder.width(), + height = placeholder.height(); + surface.resize(width, height); + overlay.resize(width, height); + }; + + // public attributes + plot.hooks = hooks; + + // initialize + initPlugins(plot); + parseOptions(options_); + setupCanvases(); + setData(data_); + setupGrid(); + draw(); + bindEvents(); + + + function executeHooks(hook, args) { + args = [plot].concat(args); + for (var i = 0; i < hook.length; ++i) + hook[i].apply(this, args); + } + + function initPlugins() { + + // References to key classes, allowing plugins to modify them + + var classes = { + Canvas: Canvas + }; + + for (var i = 0; i < plugins.length; ++i) { + var p = plugins[i]; + p.init(plot, classes); + if (p.options) + $.extend(true, options, p.options); + } + } + + function parseOptions(opts) { + + $.extend(true, options, opts); + + // $.extend merges arrays, rather than replacing them. When less + // colors are provided than the size of the default palette, we + // end up with those colors plus the remaining defaults, which is + // not expected behavior; avoid it by replacing them here. + + if (opts && opts.colors) { + options.colors = opts.colors; + } + + if (options.xaxis.color == null) + options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + if (options.yaxis.color == null) + options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility + options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; + if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility + options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; + + if (options.grid.borderColor == null) + options.grid.borderColor = options.grid.color; + if (options.grid.tickColor == null) + options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + // Fill in defaults for axis options, including any unspecified + // font-spec fields, if a font-spec was provided. + + // If no x/y axis options were provided, create one of each anyway, + // since the rest of the code assumes that they exist. + + var i, axisOptions, axisCount, + fontSize = placeholder.css("font-size"), + fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, + fontDefaults = { + style: placeholder.css("font-style"), + size: Math.round(0.8 * fontSizeDefault), + variant: placeholder.css("font-variant"), + weight: placeholder.css("font-weight"), + family: placeholder.css("font-family") + }; + + axisCount = options.xaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.xaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.xaxis, axisOptions); + options.xaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + axisCount = options.yaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.yaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.yaxis, axisOptions); + options.yaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + // backwards compatibility, to be removed in future + if (options.xaxis.noTicks && options.xaxis.ticks == null) + options.xaxis.ticks = options.xaxis.noTicks; + if (options.yaxis.noTicks && options.yaxis.ticks == null) + options.yaxis.ticks = options.yaxis.noTicks; + if (options.x2axis) { + options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); + options.xaxes[1].position = "top"; + // Override the inherit to allow the axis to auto-scale + if (options.x2axis.min == null) { + options.xaxes[1].min = null; + } + if (options.x2axis.max == null) { + options.xaxes[1].max = null; + } + } + if (options.y2axis) { + options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); + options.yaxes[1].position = "right"; + // Override the inherit to allow the axis to auto-scale + if (options.y2axis.min == null) { + options.yaxes[1].min = null; + } + if (options.y2axis.max == null) { + options.yaxes[1].max = null; + } + } + if (options.grid.coloredAreas) + options.grid.markings = options.grid.coloredAreas; + if (options.grid.coloredAreasColor) + options.grid.markingsColor = options.grid.coloredAreasColor; + if (options.lines) + $.extend(true, options.series.lines, options.lines); + if (options.points) + $.extend(true, options.series.points, options.points); + if (options.bars) + $.extend(true, options.series.bars, options.bars); + if (options.shadowSize != null) + options.series.shadowSize = options.shadowSize; + if (options.highlightColor != null) + options.series.highlightColor = options.highlightColor; + + // save options on axes for future reference + for (i = 0; i < options.xaxes.length; ++i) + getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; + for (i = 0; i < options.yaxes.length; ++i) + getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; + + // add hooks from options + for (var n in hooks) + if (options.hooks[n] && options.hooks[n].length) + hooks[n] = hooks[n].concat(options.hooks[n]); + + executeHooks(hooks.processOptions, [options]); + } + + function setData(d) { + series = parseData(d); + fillInSeriesOptions(); + processData(); + } + + function parseData(d) { + var res = []; + for (var i = 0; i < d.length; ++i) { + var s = $.extend(true, {}, options.series); + + if (d[i].data != null) { + s.data = d[i].data; // move the data instead of deep-copy + delete d[i].data; + + $.extend(true, s, d[i]); + + d[i].data = s.data; + } + else + s.data = d[i]; + res.push(s); + } + + return res; + } + + function axisNumber(obj, coord) { + var a = obj[coord + "axis"]; + if (typeof a == "object") // if we got a real axis, extract number + a = a.n; + if (typeof a != "number") + a = 1; // default to first axis + return a; + } + + function allAxes() { + // return flat array without annoying null entries + return $.grep(xaxes.concat(yaxes), function (a) { return a; }); + } + + function canvasToAxisCoords(pos) { + // return an object with x/y corresponding to all used axes + var res = {}, i, axis; + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) + res["x" + axis.n] = axis.c2p(pos.left); + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) + res["y" + axis.n] = axis.c2p(pos.top); + } + + if (res.x1 !== undefined) + res.x = res.x1; + if (res.y1 !== undefined) + res.y = res.y1; + + return res; + } + + function axisToCanvasCoords(pos) { + // get canvas coords from the first pair of x/y found in pos + var res = {}, i, axis, key; + + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) { + key = "x" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "x"; + + if (pos[key] != null) { + res.left = axis.p2c(pos[key]); + break; + } + } + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) { + key = "y" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "y"; + + if (pos[key] != null) { + res.top = axis.p2c(pos[key]); + break; + } + } + } + + return res; + } + + function getOrCreateAxis(axes, number) { + if (!axes[number - 1]) + axes[number - 1] = { + n: number, // save the number for future reference + direction: axes == xaxes ? "x" : "y", + options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) + }; + + return axes[number - 1]; + } + + function fillInSeriesOptions() { + + var neededColors = series.length, maxIndex = -1, i; + + // Subtract the number of series that already have fixed colors or + // color indexes from the number that we still need to generate. + + for (i = 0; i < series.length; ++i) { + var sc = series[i].color; + if (sc != null) { + neededColors--; + if (typeof sc == "number" && sc > maxIndex) { + maxIndex = sc; + } + } + } + + // If any of the series have fixed color indexes, then we need to + // generate at least as many colors as the highest index. + + if (neededColors <= maxIndex) { + neededColors = maxIndex + 1; + } + + // Generate all the colors, using first the option colors and then + // variations on those colors once they're exhausted. + + var c, colors = [], colorPool = options.colors, + colorPoolSize = colorPool.length, variation = 0; + + for (i = 0; i < neededColors; i++) { + + c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); + + // Each time we exhaust the colors in the pool we adjust + // a scaling factor used to produce more variations on + // those colors. The factor alternates negative/positive + // to produce lighter/darker colors. + + // Reset the variation after every few cycles, or else + // it will end up producing only white or black colors. + + if (i % colorPoolSize == 0 && i) { + if (variation >= 0) { + if (variation < 0.5) { + variation = -variation - 0.2; + } else variation = 0; + } else variation = -variation; + } + + colors[i] = c.scale('rgb', 1 + variation); + } + + // Finalize the series options, filling in their colors + + var colori = 0, s; + for (i = 0; i < series.length; ++i) { + s = series[i]; + + // assign colors + if (s.color == null) { + s.color = colors[colori].toString(); + ++colori; + } + else if (typeof s.color == "number") + s.color = colors[s.color].toString(); + + // turn on lines automatically in case nothing is set + if (s.lines.show == null) { + var v, show = true; + for (v in s) + if (s[v] && s[v].show) { + show = false; + break; + } + if (show) + s.lines.show = true; + } + + // If nothing was provided for lines.zero, default it to match + // lines.fill, since areas by default should extend to zero. + + if (s.lines.zero == null) { + s.lines.zero = !!s.lines.fill; + } + + // setup axes + s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); + s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); + } + } + + function processData() { + var topSentry = Number.POSITIVE_INFINITY, + bottomSentry = Number.NEGATIVE_INFINITY, + fakeInfinity = Number.MAX_VALUE, + i, j, k, m, length, + s, points, ps, x, y, axis, val, f, p, + data, format; + + function updateAxis(axis, min, max) { + if (min < axis.datamin && min != -fakeInfinity) + axis.datamin = min; + if (max > axis.datamax && max != fakeInfinity) + axis.datamax = max; + } + + $.each(allAxes(), function (_, axis) { + // init axis + axis.datamin = topSentry; + axis.datamax = bottomSentry; + axis.used = false; + }); + + for (i = 0; i < series.length; ++i) { + s = series[i]; + s.datapoints = { points: [] }; + + executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); + } + + // first pass: clean and copy data + for (i = 0; i < series.length; ++i) { + s = series[i]; + + data = s.data; + format = s.datapoints.format; + + if (!format) { + format = []; + // find out how to copy + format.push({ x: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + + if (s.bars.show || (s.lines.show && s.lines.fill)) { + var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); + format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } + + s.datapoints.format = format; + } + + if (s.datapoints.pointsize != null) + continue; // already filled in + + s.datapoints.pointsize = format.length; + + ps = s.datapoints.pointsize; + points = s.datapoints.points; + + var insertSteps = s.lines.show && s.lines.steps; + s.xaxis.used = s.yaxis.used = true; + + for (j = k = 0; j < data.length; ++j, k += ps) { + p = data[j]; + + var nullify = p == null; + if (!nullify) { + for (m = 0; m < ps; ++m) { + val = p[m]; + f = format[m]; + + if (f) { + if (f.number && val != null) { + val = +val; // convert to number + if (isNaN(val)) + val = null; + else if (val == Infinity) + val = fakeInfinity; + else if (val == -Infinity) + val = -fakeInfinity; + } + + if (val == null) { + if (f.required) + nullify = true; + + if (f.defaultValue != null) + val = f.defaultValue; + } + } + + points[k + m] = val; + } + } + + if (nullify) { + for (m = 0; m < ps; ++m) { + val = points[k + m]; + if (val != null) { + f = format[m]; + // extract min/max info + if (f.autoscale !== false) { + if (f.x) { + updateAxis(s.xaxis, val, val); + } + if (f.y) { + updateAxis(s.yaxis, val, val); + } + } + } + points[k + m] = null; + } + } + else { + // a little bit of line specific stuff that + // perhaps shouldn't be here, but lacking + // better means... + if (insertSteps && k > 0 + && points[k - ps] != null + && points[k - ps] != points[k] + && points[k - ps + 1] != points[k + 1]) { + // copy the point to make room for a middle point + for (m = 0; m < ps; ++m) + points[k + ps + m] = points[k + m]; + + // middle point has same y + points[k + 1] = points[k - ps + 1]; + + // we've added a point, better reflect that + k += ps; + } + } + } + } + + // give the hooks a chance to run + for (i = 0; i < series.length; ++i) { + s = series[i]; + + executeHooks(hooks.processDatapoints, [ s, s.datapoints]); + } + + // second pass: find datamax/datamin for auto-scaling + for (i = 0; i < series.length; ++i) { + s = series[i]; + points = s.datapoints.points; + ps = s.datapoints.pointsize; + format = s.datapoints.format; + + var xmin = topSentry, ymin = topSentry, + xmax = bottomSentry, ymax = bottomSentry; + + for (j = 0; j < points.length; j += ps) { + if (points[j] == null) + continue; + + for (m = 0; m < ps; ++m) { + val = points[j + m]; + f = format[m]; + if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) + continue; + + if (f.x) { + if (val < xmin) + xmin = val; + if (val > xmax) + xmax = val; + } + if (f.y) { + if (val < ymin) + ymin = val; + if (val > ymax) + ymax = val; + } + } + } + + if (s.bars.show) { + // make sure we got room for the bar on the dancing floor + var delta; + + switch (s.bars.align) { + case "left": + delta = 0; + break; + case "right": + delta = -s.bars.barWidth; + break; + default: + delta = -s.bars.barWidth / 2; + } + + if (s.bars.horizontal) { + ymin += delta; + ymax += delta + s.bars.barWidth; + } + else { + xmin += delta; + xmax += delta + s.bars.barWidth; + } + } + + updateAxis(s.xaxis, xmin, xmax); + updateAxis(s.yaxis, ymin, ymax); + } + + $.each(allAxes(), function (_, axis) { + if (axis.datamin == topSentry) + axis.datamin = null; + if (axis.datamax == bottomSentry) + axis.datamax = null; + }); + } + + function setupCanvases() { + + // Make sure the placeholder is clear of everything except canvases + // from a previous plot in this container that we'll try to re-use. + + placeholder.css("padding", 0) // padding messes up the positioning + .children().filter(function(){ + return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); + }).remove(); + + if (placeholder.css("position") == 'static') + placeholder.css("position", "relative"); // for positioning labels and overlay + + surface = new Canvas("flot-base", placeholder); + overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features + + ctx = surface.context; + octx = overlay.context; + + // define which element we're listening for events on + eventHolder = $(overlay.element).unbind(); + + // If we're re-using a plot object, shut down the old one + + var existing = placeholder.data("plot"); + + if (existing) { + existing.shutdown(); + overlay.clear(); + } + + // save in case we get replotted + placeholder.data("plot", plot); + } + + function bindEvents() { + // bind events + if (options.grid.hoverable) { + eventHolder.mousemove(onMouseMove); + + // Use bind, rather than .mouseleave, because we officially + // still support jQuery 1.2.6, which doesn't define a shortcut + // for mouseenter or mouseleave. This was a bug/oversight that + // was fixed somewhere around 1.3.x. We can return to using + // .mouseleave when we drop support for 1.2.6. + + eventHolder.bind("mouseleave", onMouseLeave); + } + + if (options.grid.clickable) + eventHolder.click(onClick); + + executeHooks(hooks.bindEvents, [eventHolder]); + } + + function shutdown() { + if (redrawTimeout) + clearTimeout(redrawTimeout); + + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mouseleave", onMouseLeave); + eventHolder.unbind("click", onClick); + + executeHooks(hooks.shutdown, [eventHolder]); + } + + function setTransformationHelpers(axis) { + // set helper functions on the axis, assumes plot area + // has been computed already + + function identity(x) { return x; } + + var s, m, t = axis.options.transform || identity, + it = axis.options.inverseTransform; + + // precompute how much the axis is scaling a point + // in canvas space + if (axis.direction == "x") { + s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); + m = Math.min(t(axis.max), t(axis.min)); + } + else { + s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); + s = -s; + m = Math.max(t(axis.max), t(axis.min)); + } + + // data point to canvas coordinate + if (t == identity) // slight optimization + axis.p2c = function (p) { return (p - m) * s; }; + else + axis.p2c = function (p) { return (t(p) - m) * s; }; + // canvas coordinate to data point + if (!it) + axis.c2p = function (c) { return m + c / s; }; + else + axis.c2p = function (c) { return it(m + c / s); }; + } + + function measureTickLabels(axis) { + + var opts = axis.options, + ticks = axis.ticks || [], + labelWidth = opts.labelWidth || 0, + labelHeight = opts.labelHeight || 0, + maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = opts.font || "flot-tick-label tickLabel"; + + for (var i = 0; i < ticks.length; ++i) { + + var t = ticks[i]; + + if (!t.label) + continue; + + var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); + + labelWidth = Math.max(labelWidth, info.width); + labelHeight = Math.max(labelHeight, info.height); + } + + axis.labelWidth = opts.labelWidth || labelWidth; + axis.labelHeight = opts.labelHeight || labelHeight; + } + + function allocateAxisBoxFirstPhase(axis) { + // find the bounding box of the axis by looking at label + // widths/heights and ticks, make room by diminishing the + // plotOffset; this first phase only looks at one + // dimension per axis, the other dimension depends on the + // other axes so will have to wait + + var lw = axis.labelWidth, + lh = axis.labelHeight, + pos = axis.options.position, + isXAxis = axis.direction === "x", + tickLength = axis.options.tickLength, + axisMargin = options.grid.axisMargin, + padding = options.grid.labelMargin, + innermost = true, + outermost = true, + first = true, + found = false; + + // Determine the axis's position in its direction and on its side + + $.each(isXAxis ? xaxes : yaxes, function(i, a) { + if (a && (a.show || a.reserveSpace)) { + if (a === axis) { + found = true; + } else if (a.options.position === pos) { + if (found) { + outermost = false; + } else { + innermost = false; + } + } + if (!found) { + first = false; + } + } + }); + + // The outermost axis on each side has no margin + + if (outermost) { + axisMargin = 0; + } + + // The ticks for the first axis in each direction stretch across + + if (tickLength == null) { + tickLength = first ? "full" : 5; + } + + if (!isNaN(+tickLength)) + padding += +tickLength; + + if (isXAxis) { + lh += padding; + + if (pos == "bottom") { + plotOffset.bottom += lh + axisMargin; + axis.box = { top: surface.height - plotOffset.bottom, height: lh }; + } + else { + axis.box = { top: plotOffset.top + axisMargin, height: lh }; + plotOffset.top += lh + axisMargin; + } + } + else { + lw += padding; + + if (pos == "left") { + axis.box = { left: plotOffset.left + axisMargin, width: lw }; + plotOffset.left += lw + axisMargin; + } + else { + plotOffset.right += lw + axisMargin; + axis.box = { left: surface.width - plotOffset.right, width: lw }; + } + } + + // save for future reference + axis.position = pos; + axis.tickLength = tickLength; + axis.box.padding = padding; + axis.innermost = innermost; + } + + function allocateAxisBoxSecondPhase(axis) { + // now that all axis boxes have been placed in one + // dimension, we can set the remaining dimension coordinates + if (axis.direction == "x") { + axis.box.left = plotOffset.left - axis.labelWidth / 2; + axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; + } + else { + axis.box.top = plotOffset.top - axis.labelHeight / 2; + axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; + } + } + + function adjustLayoutForThingsStickingOut() { + // possibly adjust plot offset to ensure everything stays + // inside the canvas and isn't clipped off + + var minMargin = options.grid.minBorderMargin, + axis, i; + + // check stuff from the plot (FIXME: this should just read + // a value from the series, otherwise it's impossible to + // customize) + if (minMargin == null) { + minMargin = 0; + for (i = 0; i < series.length; ++i) + minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); + } + + var margins = { + left: minMargin, + right: minMargin, + top: minMargin, + bottom: minMargin + }; + + // check axis labels, note we don't check the actual + // labels but instead use the overall width/height to not + // jump as much around with replots + $.each(allAxes(), function (_, axis) { + if (axis.reserveSpace && axis.ticks && axis.ticks.length) { + if (axis.direction === "x") { + margins.left = Math.max(margins.left, axis.labelWidth / 2); + margins.right = Math.max(margins.right, axis.labelWidth / 2); + } else { + margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); + margins.top = Math.max(margins.top, axis.labelHeight / 2); + } + } + }); + + plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); + plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); + plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); + plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); + } + + function setupGrid() { + var i, axes = allAxes(), showGrid = options.grid.show; + + // Initialize the plot's offset from the edge of the canvas + + for (var a in plotOffset) { + var margin = options.grid.margin || 0; + plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; + } + + executeHooks(hooks.processOffset, [plotOffset]); + + // If the grid is visible, add its border width to the offset + + for (var a in plotOffset) { + if(typeof(options.grid.borderWidth) == "object") { + plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; + } + else { + plotOffset[a] += showGrid ? options.grid.borderWidth : 0; + } + } + + $.each(axes, function (_, axis) { + var axisOpts = axis.options; + axis.show = axisOpts.show == null ? axis.used : axisOpts.show; + axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; + setRange(axis); + }); + + if (showGrid) { + + var allocatedAxes = $.grep(axes, function (axis) { + return axis.show || axis.reserveSpace; + }); + + $.each(allocatedAxes, function (_, axis) { + // make the ticks + setupTickGeneration(axis); + setTicks(axis); + snapRangeToTicks(axis, axis.ticks); + // find labelWidth/Height for axis + measureTickLabels(axis); + }); + + // with all dimensions calculated, we can compute the + // axis bounding boxes, start from the outside + // (reverse order) + for (i = allocatedAxes.length - 1; i >= 0; --i) + allocateAxisBoxFirstPhase(allocatedAxes[i]); + + // make sure we've got enough space for things that + // might stick out + adjustLayoutForThingsStickingOut(); + + $.each(allocatedAxes, function (_, axis) { + allocateAxisBoxSecondPhase(axis); + }); + } + + plotWidth = surface.width - plotOffset.left - plotOffset.right; + plotHeight = surface.height - plotOffset.bottom - plotOffset.top; + + // now we got the proper plot dimensions, we can compute the scaling + $.each(axes, function (_, axis) { + setTransformationHelpers(axis); + }); + + if (showGrid) { + drawAxisLabels(); + } + + insertLegend(); + } + + function setRange(axis) { + var opts = axis.options, + min = +(opts.min != null ? opts.min : axis.datamin), + max = +(opts.max != null ? opts.max : axis.datamax), + delta = max - min; + + if (delta == 0.0) { + // degenerate case + var widen = max == 0 ? 1 : 0.01; + + if (opts.min == null) + min -= widen; + // always widen max if we couldn't widen min to ensure we + // don't fall into min == max which doesn't work + if (opts.max == null || opts.min != null) + max += widen; + } + else { + // consider autoscaling + var margin = opts.autoscaleMargin; + if (margin != null) { + if (opts.min == null) { + min -= delta * margin; + // make sure we don't go below zero if all values + // are positive + if (min < 0 && axis.datamin != null && axis.datamin >= 0) + min = 0; + } + if (opts.max == null) { + max += delta * margin; + if (max > 0 && axis.datamax != null && axis.datamax <= 0) + max = 0; + } + } + } + axis.min = min; + axis.max = max; + } + + function setupTickGeneration(axis) { + var opts = axis.options; + + // estimate number of ticks + var noTicks; + if (typeof opts.ticks == "number" && opts.ticks > 0) + noTicks = opts.ticks; + else + // heuristic based on the model a*sqrt(x) fitted to + // some data points that seemed reasonable + noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); + + var delta = (axis.max - axis.min) / noTicks, + dec = -Math.floor(Math.log(delta) / Math.LN10), + maxDec = opts.tickDecimals; + + if (maxDec != null && dec > maxDec) { + dec = maxDec; + } + + var magn = Math.pow(10, -dec), + norm = delta / magn, // norm is between 1.0 and 10.0 + size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + if (opts.minTickSize != null && size < opts.minTickSize) { + size = opts.minTickSize; + } + + axis.delta = delta; + axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + axis.tickSize = opts.tickSize || size; + + // Time mode was moved to a plug-in in 0.8, and since so many people use it + // we'll add an especially friendly reminder to make sure they included it. + + if (opts.mode == "time" && !axis.tickGenerator) { + throw new Error("Time mode requires the flot.time plugin."); + } + + // Flot supports base-10 axes; any other mode else is handled by a plug-in, + // like flot.time.js. + + if (!axis.tickGenerator) { + + axis.tickGenerator = function (axis) { + + var ticks = [], + start = floorInBase(axis.min, axis.tickSize), + i = 0, + v = Number.NaN, + prev; + + do { + prev = v; + v = start + i * axis.tickSize; + ticks.push(v); + ++i; + } while (v < axis.max && v != prev); + return ticks; + }; + + axis.tickFormatter = function (value, axis) { + + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; + var formatted = "" + Math.round(value * factor) / factor; + + // If tickDecimals was specified, ensure that we have exactly that + // much precision; otherwise default to the value's own precision. + + if (axis.tickDecimals != null) { + var decimal = formatted.indexOf("."); + var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; + if (precision < axis.tickDecimals) { + return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); + } + } + + return formatted; + }; + } + + if ($.isFunction(opts.tickFormatter)) + axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; + + if (opts.alignTicksWithAxis != null) { + var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; + if (otherAxis && otherAxis.used && otherAxis != axis) { + // consider snapping min/max to outermost nice ticks + var niceTicks = axis.tickGenerator(axis); + if (niceTicks.length > 0) { + if (opts.min == null) + axis.min = Math.min(axis.min, niceTicks[0]); + if (opts.max == null && niceTicks.length > 1) + axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); + } + + axis.tickGenerator = function (axis) { + // copy ticks, scaled to this axis + var ticks = [], v, i; + for (i = 0; i < otherAxis.ticks.length; ++i) { + v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); + v = axis.min + v * (axis.max - axis.min); + ticks.push(v); + } + return ticks; + }; + + // we might need an extra decimal since forced + // ticks don't necessarily fit naturally + if (!axis.mode && opts.tickDecimals == null) { + var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), + ts = axis.tickGenerator(axis); + + // only proceed if the tick interval rounded + // with an extra decimal doesn't give us a + // zero at end + if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) + axis.tickDecimals = extraDec; + } + } + } + } + + function setTicks(axis) { + var oticks = axis.options.ticks, ticks = []; + if (oticks == null || (typeof oticks == "number" && oticks > 0)) + ticks = axis.tickGenerator(axis); + else if (oticks) { + if ($.isFunction(oticks)) + // generate the ticks + ticks = oticks(axis); + else + ticks = oticks; + } + + // clean up/labelify the supplied ticks, copy them over + var i, v; + axis.ticks = []; + for (i = 0; i < ticks.length; ++i) { + var label = null; + var t = ticks[i]; + if (typeof t == "object") { + v = +t[0]; + if (t.length > 1) + label = t[1]; + } + else + v = +t; + if (label == null) + label = axis.tickFormatter(v, axis); + if (!isNaN(v)) + axis.ticks.push({ v: v, label: label }); + } + } + + function snapRangeToTicks(axis, ticks) { + if (axis.options.autoscaleMargin && ticks.length > 0) { + // snap to ticks + if (axis.options.min == null) + axis.min = Math.min(axis.min, ticks[0].v); + if (axis.options.max == null && ticks.length > 1) + axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); + } + } + + function draw() { + + surface.clear(); + + executeHooks(hooks.drawBackground, [ctx]); + + var grid = options.grid; + + // draw background, if any + if (grid.show && grid.backgroundColor) + drawBackground(); + + if (grid.show && !grid.aboveData) { + drawGrid(); + } + + for (var i = 0; i < series.length; ++i) { + executeHooks(hooks.drawSeries, [ctx, series[i]]); + drawSeries(series[i]); + } + + executeHooks(hooks.draw, [ctx]); + + if (grid.show && grid.aboveData) { + drawGrid(); + } + + surface.render(); + + // A draw implies that either the axes or data have changed, so we + // should probably update the overlay highlights as well. + + triggerRedrawOverlay(); + } + + function extractRange(ranges, coord) { + var axis, from, to, key, axes = allAxes(); + + for (var i = 0; i < axes.length; ++i) { + axis = axes[i]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? xaxes[0] : yaxes[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function drawBackground() { + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); + ctx.fillRect(0, 0, plotWidth, plotHeight); + ctx.restore(); + } + + function drawGrid() { + var i, axes, bw, bc; + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // draw markings + var markings = options.grid.markings; + if (markings) { + if ($.isFunction(markings)) { + axes = plot.getAxes(); + // xmin etc. is backwards compatibility, to be + // removed in the future + axes.xmin = axes.xaxis.min; + axes.xmax = axes.xaxis.max; + axes.ymin = axes.yaxis.min; + axes.ymax = axes.yaxis.max; + + markings = markings(axes); + } + + for (i = 0; i < markings.length; ++i) { + var m = markings[i], + xrange = extractRange(m, "x"), + yrange = extractRange(m, "y"); + + // fill in missing + if (xrange.from == null) + xrange.from = xrange.axis.min; + if (xrange.to == null) + xrange.to = xrange.axis.max; + if (yrange.from == null) + yrange.from = yrange.axis.min; + if (yrange.to == null) + yrange.to = yrange.axis.max; + + // clip + if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || + yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) + continue; + + xrange.from = Math.max(xrange.from, xrange.axis.min); + xrange.to = Math.min(xrange.to, xrange.axis.max); + yrange.from = Math.max(yrange.from, yrange.axis.min); + yrange.to = Math.min(yrange.to, yrange.axis.max); + + var xequal = xrange.from === xrange.to, + yequal = yrange.from === yrange.to; + + if (xequal && yequal) { + continue; + } + + // then draw + xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); + xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); + yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); + yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); + + if (xequal || yequal) { + var lineWidth = m.lineWidth || options.grid.markingsLineWidth, + subPixel = lineWidth % 2 ? 0.5 : 0; + ctx.beginPath(); + ctx.strokeStyle = m.color || options.grid.markingsColor; + ctx.lineWidth = lineWidth; + if (xequal) { + ctx.moveTo(xrange.to + subPixel, yrange.from); + ctx.lineTo(xrange.to + subPixel, yrange.to); + } else { + ctx.moveTo(xrange.from, yrange.to + subPixel); + ctx.lineTo(xrange.to, yrange.to + subPixel); + } + ctx.stroke(); + } else { + ctx.fillStyle = m.color || options.grid.markingsColor; + ctx.fillRect(xrange.from, yrange.to, + xrange.to - xrange.from, + yrange.from - yrange.to); + } + } + } + + // draw the ticks + axes = allAxes(); + bw = options.grid.borderWidth; + + for (var j = 0; j < axes.length; ++j) { + var axis = axes[j], box = axis.box, + t = axis.tickLength, x, y, xoff, yoff; + if (!axis.show || axis.ticks.length == 0) + continue; + + ctx.lineWidth = 1; + + // find the edges + if (axis.direction == "x") { + x = 0; + if (t == "full") + y = (axis.position == "top" ? 0 : plotHeight); + else + y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); + } + else { + y = 0; + if (t == "full") + x = (axis.position == "left" ? 0 : plotWidth); + else + x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); + } + + // draw tick bar + if (!axis.innermost) { + ctx.strokeStyle = axis.options.color; + ctx.beginPath(); + xoff = yoff = 0; + if (axis.direction == "x") + xoff = plotWidth + 1; + else + yoff = plotHeight + 1; + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") { + y = Math.floor(y) + 0.5; + } else { + x = Math.floor(x) + 0.5; + } + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + ctx.stroke(); + } + + // draw ticks + + ctx.strokeStyle = axis.options.tickColor; + + ctx.beginPath(); + for (i = 0; i < axis.ticks.length; ++i) { + var v = axis.ticks[i].v; + + xoff = yoff = 0; + + if (isNaN(v) || v < axis.min || v > axis.max + // skip those lying on the axes if we got a border + || (t == "full" + && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) + && (v == axis.min || v == axis.max))) + continue; + + if (axis.direction == "x") { + x = axis.p2c(v); + yoff = t == "full" ? -plotHeight : t; + + if (axis.position == "top") + yoff = -yoff; + } + else { + y = axis.p2c(v); + xoff = t == "full" ? -plotWidth : t; + + if (axis.position == "left") + xoff = -xoff; + } + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") + x = Math.floor(x) + 0.5; + else + y = Math.floor(y) + 0.5; + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + } + + ctx.stroke(); + } + + + // draw border + if (bw) { + // If either borderWidth or borderColor is an object, then draw the border + // line by line instead of as one rectangle + bc = options.grid.borderColor; + if(typeof bw == "object" || typeof bc == "object") { + if (typeof bw !== "object") { + bw = {top: bw, right: bw, bottom: bw, left: bw}; + } + if (typeof bc !== "object") { + bc = {top: bc, right: bc, bottom: bc, left: bc}; + } + + if (bw.top > 0) { + ctx.strokeStyle = bc.top; + ctx.lineWidth = bw.top; + ctx.beginPath(); + ctx.moveTo(0 - bw.left, 0 - bw.top/2); + ctx.lineTo(plotWidth, 0 - bw.top/2); + ctx.stroke(); + } + + if (bw.right > 0) { + ctx.strokeStyle = bc.right; + ctx.lineWidth = bw.right; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); + ctx.lineTo(plotWidth + bw.right / 2, plotHeight); + ctx.stroke(); + } + + if (bw.bottom > 0) { + ctx.strokeStyle = bc.bottom; + ctx.lineWidth = bw.bottom; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); + ctx.lineTo(0, plotHeight + bw.bottom / 2); + ctx.stroke(); + } + + if (bw.left > 0) { + ctx.strokeStyle = bc.left; + ctx.lineWidth = bw.left; + ctx.beginPath(); + ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); + ctx.lineTo(0- bw.left/2, 0); + ctx.stroke(); + } + } + else { + ctx.lineWidth = bw; + ctx.strokeStyle = options.grid.borderColor; + ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); + } + } + + ctx.restore(); + } + + function drawAxisLabels() { + + $.each(allAxes(), function (_, axis) { + var box = axis.box, + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = axis.options.font || "flot-tick-label tickLabel", + tick, x, y, halign, valign; + + // Remove text before checking for axis.show and ticks.length; + // otherwise plugins, like flot-tickrotor, that draw their own + // tick labels will end up with both theirs and the defaults. + + surface.removeText(layer); + + if (!axis.show || axis.ticks.length == 0) + return; + + for (var i = 0; i < axis.ticks.length; ++i) { + + tick = axis.ticks[i]; + if (!tick.label || tick.v < axis.min || tick.v > axis.max) + continue; + + if (axis.direction == "x") { + halign = "center"; + x = plotOffset.left + axis.p2c(tick.v); + if (axis.position == "bottom") { + y = box.top + box.padding; + } else { + y = box.top + box.height - box.padding; + valign = "bottom"; + } + } else { + valign = "middle"; + y = plotOffset.top + axis.p2c(tick.v); + if (axis.position == "left") { + x = box.left + box.width - box.padding; + halign = "right"; + } else { + x = box.left + box.padding; + } + } + + surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); + } + }); + } + + function drawSeries(series) { + if (series.lines.show) + drawSeriesLines(series); + if (series.bars.show) + drawSeriesBars(series); + if (series.points.show) + drawSeriesPoints(series); + } + + function drawSeriesLines(series) { + function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + prevx = null, prevy = null; + + ctx.beginPath(); + for (var i = ps; i < points.length; i += ps) { + var x1 = points[i - ps], y1 = points[i - ps + 1], + x2 = points[i], y2 = points[i + 1]; + + if (x1 == null || x2 == null) + continue; + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min) { + if (y2 < axisy.min) + continue; // line segment is outside + // compute new intersection point + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min) { + if (y1 < axisy.min) + continue; + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max) { + if (y2 > axisy.max) + continue; + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max) { + if (y1 > axisy.max) + continue; + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (x1 != prevx || y1 != prevy) + ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); + + prevx = x2; + prevy = y2; + ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); + } + ctx.stroke(); + } + + function plotLineArea(datapoints, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + bottom = Math.min(Math.max(0, axisy.min), axisy.max), + i = 0, top, areaOpen = false, + ypos = 1, segmentStart = 0, segmentEnd = 0; + + // we process each segment in two turns, first forward + // direction to sketch out top, then once we hit the + // end we go backwards to sketch the bottom + while (true) { + if (ps > 0 && i > points.length + ps) + break; + + i += ps; // ps is negative if going backwards + + var x1 = points[i - ps], + y1 = points[i - ps + ypos], + x2 = points[i], y2 = points[i + ypos]; + + if (areaOpen) { + if (ps > 0 && x1 != null && x2 == null) { + // at turning point + segmentEnd = i; + ps = -ps; + ypos = 2; + continue; + } + + if (ps < 0 && i == segmentStart + ps) { + // done with the reverse sweep + ctx.fill(); + areaOpen = false; + ps = -ps; + ypos = 1; + i = segmentStart = segmentEnd + ps; + continue; + } + } + + if (x1 == null || x2 == null) + continue; + + // clip x values + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (!areaOpen) { + // open area + ctx.beginPath(); + ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); + areaOpen = true; + } + + // now first check the case where both is outside + if (y1 >= axisy.max && y2 >= axisy.max) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); + continue; + } + else if (y1 <= axisy.min && y2 <= axisy.min) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); + continue; + } + + // else it's a bit more complicated, there might + // be a flat maxed out rectangle first, then a + // triangular cutout or reverse; to find these + // keep track of the current x values + var x1old = x1, x2old = x2; + + // clip the y values, without shortcutting, we + // go through all cases in turn + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // if the x value was changed we got a rectangle + // to fill + if (x1 != x1old) { + ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); + // it goes to (x1, y1), but we fill that below + } + + // fill triangular section, this sometimes result + // in redundant points if (x1, y1) hasn't changed + // from previous line to, but we just ignore that + ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + + // fill the other rectangle if it's there + if (x2 != x2old) { + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); + } + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + ctx.lineJoin = "round"; + + var lw = series.lines.lineWidth, + sw = series.shadowSize; + // FIXME: consider another form of shadow when filling is turned on + if (lw > 0 && sw > 0) { + // draw shadow as a thick and thin line with transparency + ctx.lineWidth = sw; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + // position shadow at angle from the mid of line + var angle = Math.PI/18; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); + ctx.lineWidth = sw/2; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); + if (fillStyle) { + ctx.fillStyle = fillStyle; + plotLineArea(series.datapoints, series.xaxis, series.yaxis); + } + + if (lw > 0) + plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); + ctx.restore(); + } + + function drawSeriesPoints(series) { + function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + var x = points[i], y = points[i + 1]; + if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + continue; + + ctx.beginPath(); + x = axisx.p2c(x); + y = axisy.p2c(y) + offset; + if (symbol == "circle") + ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); + else + symbol(ctx, x, y, radius, shadow); + ctx.closePath(); + + if (fillStyle) { + ctx.fillStyle = fillStyle; + ctx.fill(); + } + ctx.stroke(); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var lw = series.points.lineWidth, + sw = series.shadowSize, + radius = series.points.radius, + symbol = series.points.symbol; + + // If the user sets the line width to 0, we change it to a very + // small value. A line width of 0 seems to force the default of 1. + // Doing the conditional here allows the shadow setting to still be + // optional even with a lineWidth of 0. + + if( lw == 0 ) + lw = 0.0001; + + if (lw > 0 && sw > 0) { + // draw shadow in two steps + var w = sw / 2; + ctx.lineWidth = w; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + plotPoints(series.datapoints, radius, null, w + w/2, true, + series.xaxis, series.yaxis, symbol); + + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + plotPoints(series.datapoints, radius, null, w/2, true, + series.xaxis, series.yaxis, symbol); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + plotPoints(series.datapoints, radius, + getFillStyle(series.points, series.color), 0, false, + series.xaxis, series.yaxis, symbol); + ctx.restore(); + } + + function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { + var left, right, bottom, top, + drawLeft, drawRight, drawTop, drawBottom, + tmp; + + // in horizontal mode, we start the bar from the left + // instead of from the bottom so it appears to be + // horizontal rather than vertical + if (horizontal) { + drawBottom = drawRight = drawTop = true; + drawLeft = false; + left = b; + right = x; + top = y + barLeft; + bottom = y + barRight; + + // account for negative bars + if (right < left) { + tmp = right; + right = left; + left = tmp; + drawLeft = true; + drawRight = false; + } + } + else { + drawLeft = drawRight = drawTop = true; + drawBottom = false; + left = x + barLeft; + right = x + barRight; + bottom = b; + top = y; + + // account for negative bars + if (top < bottom) { + tmp = top; + top = bottom; + bottom = tmp; + drawBottom = true; + drawTop = false; + } + } + + // clip + if (right < axisx.min || left > axisx.max || + top < axisy.min || bottom > axisy.max) + return; + + if (left < axisx.min) { + left = axisx.min; + drawLeft = false; + } + + if (right > axisx.max) { + right = axisx.max; + drawRight = false; + } + + if (bottom < axisy.min) { + bottom = axisy.min; + drawBottom = false; + } + + if (top > axisy.max) { + top = axisy.max; + drawTop = false; + } + + left = axisx.p2c(left); + bottom = axisy.p2c(bottom); + right = axisx.p2c(right); + top = axisy.p2c(top); + + // fill the bar + if (fillStyleCallback) { + c.fillStyle = fillStyleCallback(bottom, top); + c.fillRect(left, top, right - left, bottom - top) + } + + // draw outline + if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { + c.beginPath(); + + // FIXME: inline moveTo is buggy with excanvas + c.moveTo(left, bottom); + if (drawLeft) + c.lineTo(left, top); + else + c.moveTo(left, top); + if (drawTop) + c.lineTo(right, top); + else + c.moveTo(right, top); + if (drawRight) + c.lineTo(right, bottom); + else + c.moveTo(right, bottom); + if (drawBottom) + c.lineTo(left, bottom); + else + c.moveTo(left, bottom); + c.stroke(); + } + } + + function drawSeriesBars(series) { + function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) + continue; + drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // FIXME: figure out a way to add shadows (for instance along the right edge) + ctx.lineWidth = series.bars.lineWidth; + ctx.strokeStyle = series.color; + + var barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; + plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); + ctx.restore(); + } + + function getFillStyle(filloptions, seriesColor, bottom, top) { + var fill = filloptions.fill; + if (!fill) + return null; + + if (filloptions.fillColor) + return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); + + var c = $.color.parse(seriesColor); + c.a = typeof fill == "number" ? fill : 0.4; + c.normalize(); + return c.toString(); + } + + function insertLegend() { + + if (options.legend.container != null) { + $(options.legend.container).html(""); + } else { + placeholder.find(".legend").remove(); + } + + if (!options.legend.show) { + return; + } + + var fragments = [], entries = [], rowStarted = false, + lf = options.legend.labelFormatter, s, label; + + // Build a list of legend entries, with each having a label and a color + + for (var i = 0; i < series.length; ++i) { + s = series[i]; + if (s.label) { + label = lf ? lf(s.label, s) : s.label; + if (label) { + entries.push({ + label: label, + color: s.color + }); + } + } + } + + // Sort the legend using either the default or a custom comparator + + if (options.legend.sorted) { + if ($.isFunction(options.legend.sorted)) { + entries.sort(options.legend.sorted); + } else if (options.legend.sorted == "reverse") { + entries.reverse(); + } else { + var ascending = options.legend.sorted != "descending"; + entries.sort(function(a, b) { + return a.label == b.label ? 0 : ( + (a.label < b.label) != ascending ? 1 : -1 // Logical XOR + ); + }); + } + } + + // Generate markup for the list of entries, in their final order + + for (var i = 0; i < entries.length; ++i) { + + var entry = entries[i]; + + if (i % options.legend.noColumns == 0) { + if (rowStarted) + fragments.push(''); + fragments.push(''); + rowStarted = true; + } + + fragments.push( + '
' + + '' + entry.label + '' + ); + } + + if (rowStarted) + fragments.push(''); + + if (fragments.length == 0) + return; + + var table = '' + fragments.join("") + '
'; + if (options.legend.container != null) + $(options.legend.container).html(table); + else { + var pos = "", + p = options.legend.position, + m = options.legend.margin; + if (m[0] == null) + m = [m, m]; + if (p.charAt(0) == "n") + pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; + else if (p.charAt(0) == "s") + pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; + if (p.charAt(1) == "e") + pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; + else if (p.charAt(1) == "w") + pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; + var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); + if (options.legend.backgroundOpacity != 0.0) { + // put in the transparent background + // separately to avoid blended labels and + // label boxes + var c = options.legend.backgroundColor; + if (c == null) { + c = options.grid.backgroundColor; + if (c && typeof c == "string") + c = $.color.parse(c); + else + c = $.color.extract(legend, 'background-color'); + c.a = 1; + c = c.toString(); + } + var div = legend.children(); + $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); + } + } + } + + + // interactive features + + var highlights = [], + redrawTimeout = null; + + // returns the data item the mouse is over, or null if none is found + function findNearbyItem(mouseX, mouseY, seriesFilter) { + var maxDistance = options.grid.mouseActiveRadius, + smallestDistance = maxDistance * maxDistance + 1, + item = null, foundPoint = false, i, j, ps; + + for (i = series.length - 1; i >= 0; --i) { + if (!seriesFilter(series[i])) + continue; + + var s = series[i], + axisx = s.xaxis, + axisy = s.yaxis, + points = s.datapoints.points, + mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster + my = axisy.c2p(mouseY), + maxx = maxDistance / axisx.scale, + maxy = maxDistance / axisy.scale; + + ps = s.datapoints.pointsize; + // with inverse transforms, we can't use the maxx/maxy + // optimization, sadly + if (axisx.options.inverseTransform) + maxx = Number.MAX_VALUE; + if (axisy.options.inverseTransform) + maxy = Number.MAX_VALUE; + + if (s.lines.show || s.points.show) { + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1]; + if (x == null) + continue; + + // For points and lines, the cursor must be within a + // certain distance to the data point + if (x - mx > maxx || x - mx < -maxx || + y - my > maxy || y - my < -maxy) + continue; + + // We have to calculate distances in pixels, not in + // data units, because the scales of the axes may be different + var dx = Math.abs(axisx.p2c(x) - mouseX), + dy = Math.abs(axisy.p2c(y) - mouseY), + dist = dx * dx + dy * dy; // we save the sqrt + + // use <= to ensure last point takes precedence + // (last generally means on top of) + if (dist < smallestDistance) { + smallestDistance = dist; + item = [i, j / ps]; + } + } + } + + if (s.bars.show && !item) { // no other point can be nearby + + var barLeft, barRight; + + switch (s.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -s.bars.barWidth; + break; + default: + barLeft = -s.bars.barWidth / 2; + } + + barRight = barLeft + s.bars.barWidth; + + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1], b = points[j + 2]; + if (x == null) + continue; + + // for a bar graph, the cursor must be inside the bar + if (series[i].bars.horizontal ? + (mx <= Math.max(b, x) && mx >= Math.min(b, x) && + my >= y + barLeft && my <= y + barRight) : + (mx >= x + barLeft && mx <= x + barRight && + my >= Math.min(b, y) && my <= Math.max(b, y))) + item = [i, j / ps]; + } + } + } + + if (item) { + i = item[0]; + j = item[1]; + ps = series[i].datapoints.pointsize; + + return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), + dataIndex: j, + series: series[i], + seriesIndex: i }; + } + + return null; + } + + function onMouseMove(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return s["hoverable"] != false; }); + } + + function onMouseLeave(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return false; }); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e, + function (s) { return s["clickable"] != false; }); + } + + // trigger click or hover event (they send the same parameters + // so we share their code) + function triggerClickHoverEvent(eventname, event, seriesFilter) { + var offset = eventHolder.offset(), + canvasX = event.pageX - offset.left - plotOffset.left, + canvasY = event.pageY - offset.top - plotOffset.top, + pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); + + pos.pageX = event.pageX; + pos.pageY = event.pageY; + + var item = findNearbyItem(canvasX, canvasY, seriesFilter); + + if (item) { + // fill in mouse pos for any listeners out there + item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); + item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); + } + + if (options.grid.autoHighlight) { + // clear auto-highlights + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto == eventname && + !(item && h.series == item.series && + h.point[0] == item.datapoint[0] && + h.point[1] == item.datapoint[1])) + unhighlight(h.series, h.point); + } + + if (item) + highlight(item.series, item.datapoint, eventname); + } + + placeholder.trigger(eventname, [ pos, item ]); + } + + function triggerRedrawOverlay() { + var t = options.interaction.redrawOverlayInterval; + if (t == -1) { // skip event queue + drawOverlay(); + return; + } + + if (!redrawTimeout) + redrawTimeout = setTimeout(drawOverlay, t); + } + + function drawOverlay() { + redrawTimeout = null; + + // draw highlights + octx.save(); + overlay.clear(); + octx.translate(plotOffset.left, plotOffset.top); + + var i, hi; + for (i = 0; i < highlights.length; ++i) { + hi = highlights[i]; + + if (hi.series.bars.show) + drawBarHighlight(hi.series, hi.point); + else + drawPointHighlight(hi.series, hi.point); + } + octx.restore(); + + executeHooks(hooks.drawOverlay, [octx]); + } + + function highlight(s, point, auto) { + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i == -1) { + highlights.push({ series: s, point: point, auto: auto }); + + triggerRedrawOverlay(); + } + else if (!auto) + highlights[i].auto = false; + } + + function unhighlight(s, point) { + if (s == null && point == null) { + highlights = []; + triggerRedrawOverlay(); + return; + } + + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i != -1) { + highlights.splice(i, 1); + + triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s, p) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s && h.point[0] == p[0] + && h.point[1] == p[1]) + return i; + } + return -1; + } + + function drawPointHighlight(series, point) { + var x = point[0], y = point[1], + axisx = series.xaxis, axisy = series.yaxis, + highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); + + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + return; + + var pointRadius = series.points.radius + series.points.lineWidth / 2; + octx.lineWidth = pointRadius; + octx.strokeStyle = highlightColor; + var radius = 1.5 * pointRadius; + x = axisx.p2c(x); + y = axisy.p2c(y); + + octx.beginPath(); + if (series.points.symbol == "circle") + octx.arc(x, y, radius, 0, 2 * Math.PI, false); + else + series.points.symbol(octx, x, y, radius, false); + octx.closePath(); + octx.stroke(); + } + + function drawBarHighlight(series, point) { + var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), + fillStyle = highlightColor, + barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + octx.lineWidth = series.bars.lineWidth; + octx.strokeStyle = highlightColor; + + drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, + function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); + } + + function getColorOrGradient(spec, bottom, top, defaultColor) { + if (typeof spec == "string") + return spec; + else { + // assume this is a gradient spec; IE currently only + // supports a simple vertical gradient properly, so that's + // what we support too + var gradient = ctx.createLinearGradient(0, top, 0, bottom); + + for (var i = 0, l = spec.colors.length; i < l; ++i) { + var c = spec.colors[i]; + if (typeof c != "string") { + var co = $.color.parse(defaultColor); + if (c.brightness != null) + co = co.scale('rgb', c.brightness); + if (c.opacity != null) + co.a *= c.opacity; + c = co.toString(); + } + gradient.addColorStop(i / (l - 1), c); + } + + return gradient; + } + } + } + + // Add the plot function to the top level of the jQuery object + + $.plot = function(placeholder, data, options) { + //var t0 = new Date(); + var plot = new Plot($(placeholder), data, options, $.plot.plugins); + //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); + return plot; + }; + + $.plot.version = "0.8.3"; + + $.plot.plugins = []; + + // Also add the plot function as a chainable property + + $.fn.plot = function(data, options) { + return this.each(function() { + $.plot(this, data, options); + }); + }; + + // round to nearby lower multiple of base + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.log.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.log.js new file mode 100644 index 0000000000000..e32bf5cf7e817 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.log.js @@ -0,0 +1,163 @@ +/* @notice + * + * Pretty handling of logarithmic axes. + * Copyright (c) 2007-2014 IOLA and Ole Laursen. + * Licensed under the MIT license. + * Created by Arne de Laat + * Set axis.mode to "log" and make the axis logarithmic using transform: + * axis: { + * mode: 'log', + * transform: function(v) {v <= 0 ? Math.log(v) / Math.LN10 : null}, + * inverseTransform: function(v) {Math.pow(10, v)} + * } + * The transform filters negative and zero values, because those are + * invalid on logarithmic scales. + * This plugin tries to create good looking logarithmic ticks, using + * unicode superscript characters. If all data to be plotted is between two + * powers of ten then the default flot tick generator and renderer are + * used. Logarithmic ticks are places at powers of ten and at half those + * values if there are not to many ticks already (e.g. [1, 5, 10, 50, 100]). + * For details, see https://github.com/flot/flot/pull/1328 +*/ + +(function($) { + + function log10(value) { + /* Get the Log10 of the value + */ + return Math.log(value) / Math.LN10; + } + + function floorAsLog10(value) { + /* Get power of the first power of 10 below the value + */ + return Math.floor(log10(value)); + } + + function ceilAsLog10(value) { + /* Get power of the first power of 10 above the value + */ + return Math.ceil(log10(value)); + } + + + // round to nearby lower multiple of base + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + + function getUnicodePower(power) { + var superscripts = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"], + result = "", + str_power = "" + power; + for (var i = 0; i < str_power.length; i++) { + if (str_power[i] === "+") { + } + else if (str_power[i] === "-") { + result += "⁻"; + } + else { + result += superscripts[str_power[i]]; + } + } + return result; + } + + function init(plot) { + plot.hooks.processOptions.push(function (plot) { + $.each(plot.getAxes(), function(axisName, axis) { + + var opts = axis.options; + + if (opts.mode === "log") { + + axis.tickGenerator = function (axis) { + + var ticks = [], + end = ceilAsLog10(axis.max), + start = floorAsLog10(axis.min), + tick = Number.NaN, + i = 0; + + if (axis.min === null || axis.min <= 0) { + // Bad minimum, make ticks from 1 (10**0) to max + start = 0; + axis.min = 0.6; + } + + if (end <= start) { + // Start less than end?! + ticks = [1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, + 1e7, 1e8, 1e9]; + } + else if (log10(axis.max) - log10(axis.datamin) < 1) { + // Default flot generator incase no powers of 10 + // are between start and end + var prev; + start = floorInBase(axis.min, axis.tickSize); + do { + prev = tick; + tick = start + i * axis.tickSize; + ticks.push(tick); + ++i; + } while (tick < axis.max && tick !== prev); + } + else { + // Make ticks at each power of ten + for (; i <= (end - start); i++) { + tick = Math.pow(10, start + i); + ticks.push(tick); + } + + var length = ticks.length; + + // If not to many ticks also put a tick between + // the powers of ten + if (end - start < 6) { + for (var j = 1; j < length * 2 - 1; j += 2) { + tick = ticks[j - 1] * 5; + ticks.splice(j, 0, tick); + } + } + } + return ticks; + }; + + axis.tickFormatter = function (value, axis) { + var formatted; + if (log10(axis.max) - log10(axis.datamin) < 1) { + // Default flot formatter + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; + formatted = "" + Math.round(value * factor) / factor; + if (axis.tickDecimals !== null) { + var decimal = formatted.indexOf("."); + var precision = decimal === -1 ? 0 : formatted.length - decimal - 1; + if (precision < axis.tickDecimals) { + return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); + } + } + } + else { + var multiplier = "", + exponential = parseFloat(value).toExponential(0), + power = getUnicodePower(exponential.slice(2)); + if (exponential[0] !== "1") { + multiplier = exponential[0] + "x"; + } + formatted = multiplier + "10" + power; + } + return formatted; + }; + } + }); + }); + } + + $.plot.plugins.push({ + init: init, + name: "log", + version: "0.9" + }); + +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.navigate.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.navigate.js new file mode 100644 index 0000000000000..13fb7f17d04b2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.navigate.js @@ -0,0 +1,346 @@ +/* Flot plugin for adding the ability to pan and zoom the plot. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The default behaviour is double click and scrollwheel up/down to zoom in, drag +to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and +plot.pan( offset ) so you easily can add custom controls. It also fires +"plotpan" and "plotzoom" events, useful for synchronizing plots. + +The plugin supports these options: + + zoom: { + interactive: false + trigger: "dblclick" // or "click" for single click + amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out) + } + + pan: { + interactive: false + cursor: "move" // CSS mouse cursor value used when dragging, e.g. "pointer" + frameRate: 20 + } + + xaxis, yaxis, x2axis, y2axis: { + zoomRange: null // or [ number, number ] (min range, max range) or false + panRange: null // or [ number, number ] (min, max) or false + } + +"interactive" enables the built-in drag/click behaviour. If you enable +interactive for pan, then you'll have a basic plot that supports moving +around; the same for zoom. + +"amount" specifies the default amount to zoom in (so 1.5 = 150%) relative to +the current viewport. + +"cursor" is a standard CSS mouse cursor string used for visual feedback to the +user when dragging. + +"frameRate" specifies the maximum number of times per second the plot will +update itself while the user is panning around on it (set to null to disable +intermediate pans, the plot will then not update until the mouse button is +released). + +"zoomRange" is the interval in which zooming can happen, e.g. with zoomRange: +[1, 100] the zoom will never scale the axis so that the difference between min +and max is smaller than 1 or larger than 100. You can set either end to null +to ignore, e.g. [1, null]. If you set zoomRange to false, zooming on that axis +will be disabled. + +"panRange" confines the panning to stay within a range, e.g. with panRange: +[-10, 20] panning stops at -10 in one end and at 20 in the other. Either can +be null, e.g. [-10, null]. If you set panRange to false, panning on that axis +will be disabled. + +Example API usage: + + plot = $.plot(...); + + // zoom default amount in on the pixel ( 10, 20 ) + plot.zoom({ center: { left: 10, top: 20 } }); + + // zoom out again + plot.zoomOut({ center: { left: 10, top: 20 } }); + + // zoom 200% in on the pixel (10, 20) + plot.zoom({ amount: 2, center: { left: 10, top: 20 } }); + + // pan 100 pixels to the left and 20 down + plot.pan({ left: -100, top: 20 }) + +Here, "center" specifies where the center of the zooming should happen. Note +that this is defined in pixel space, not the space of the data points (you can +use the p2c helpers on the axes in Flot to help you convert between these). + +"amount" is the amount to zoom the viewport relative to the current range, so +1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You +can set the default in the options. + +*/ + +// First two dependencies, jquery.event.drag.js and +// jquery.mousewheel.js, we put them inline here to save people the +// effort of downloading them. + +/* +jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com) +Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt +*/ +(function(a){function e(h){var k,j=this,l=h.data||{};if(l.elem)j=h.dragTarget=l.elem,h.dragProxy=d.proxy||j,h.cursorOffsetX=l.pageX-l.left,h.cursorOffsetY=l.pageY-l.top,h.offsetX=h.pageX-h.cursorOffsetX,h.offsetY=h.pageY-h.cursorOffsetY;else if(d.dragging||l.which>0&&h.which!=l.which||a(h.target).is(l.not))return;switch(h.type){case"mousedown":return a.extend(l,a(j).offset(),{elem:j,target:h.target,pageX:h.pageX,pageY:h.pageY}),b.add(document,"mousemove mouseup",e,l),i(j,!1),d.dragging=null,!1;case!d.dragging&&"mousemove":if(g(h.pageX-l.pageX)+g(h.pageY-l.pageY) max) { + // make sure min < max + var tmp = min; + min = max; + max = tmp; + } + + //Check that we are in panRange + if (pr) { + if (pr[0] != null && min < pr[0]) { + min = pr[0]; + } + if (pr[1] != null && max > pr[1]) { + max = pr[1]; + } + } + + var range = max - min; + if (zr && + ((zr[0] != null && range < zr[0] && amount >1) || + (zr[1] != null && range > zr[1] && amount <1))) + return; + + opts.min = min; + opts.max = max; + }); + + plot.setupGrid(); + plot.draw(); + + if (!args.preventEvent) + plot.getPlaceholder().trigger("plotzoom", [ plot, args ]); + }; + + plot.pan = function (args) { + var delta = { + x: +args.left, + y: +args.top + }; + + if (isNaN(delta.x)) + delta.x = 0; + if (isNaN(delta.y)) + delta.y = 0; + + $.each(plot.getAxes(), function (_, axis) { + var opts = axis.options, + min, max, d = delta[axis.direction]; + + min = axis.c2p(axis.p2c(axis.min) + d), + max = axis.c2p(axis.p2c(axis.max) + d); + + var pr = opts.panRange; + if (pr === false) // no panning on this axis + return; + + if (pr) { + // check whether we hit the wall + if (pr[0] != null && pr[0] > min) { + d = pr[0] - min; + min += d; + max += d; + } + + if (pr[1] != null && pr[1] < max) { + d = pr[1] - max; + min += d; + max += d; + } + } + + opts.min = min; + opts.max = max; + }); + + plot.setupGrid(); + plot.draw(); + + if (!args.preventEvent) + plot.getPlaceholder().trigger("plotpan", [ plot, args ]); + }; + + function shutdown(plot, eventHolder) { + eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick); + eventHolder.unbind("mousewheel", onMouseWheel); + eventHolder.unbind("dragstart", onDragStart); + eventHolder.unbind("drag", onDrag); + eventHolder.unbind("dragend", onDragEnd); + if (panTimeout) + clearTimeout(panTimeout); + } + + plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'navigate', + version: '1.3' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.pie.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.pie.js new file mode 100644 index 0000000000000..24148c0a2e223 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.pie.js @@ -0,0 +1,824 @@ +/* Flot plugin for rendering pie charts. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes that each series has a single data value, and that each +value is a positive integer or zero. Negative numbers don't make sense for a +pie chart, and have unpredictable results. The values do NOT need to be +passed in as percentages; the plugin will calculate the total and per-slice +percentages internally. + +* Created by Brian Medendorp + +* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars + +The plugin supports these options: + + series: { + pie: { + show: true/false + radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' + innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect + startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result + tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) + offset: { + top: integer value to move the pie up or down + left: integer value to move the pie left or right, or 'auto' + }, + stroke: { + color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#FFF') + width: integer pixel width of the stroke + }, + label: { + show: true/false, or 'auto' + formatter: a user-defined function that modifies the text/style of the label text + radius: 0-1 for percentage of fullsize, or a specified pixel length + background: { + color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#000') + opacity: 0-1 + }, + threshold: 0-1 for the percentage value at which to hide labels (if they're too small) + }, + combine: { + threshold: 0-1 for the percentage value at which to combine slices (if they're too small) + color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined + label: any text value of what the combined slice should be labeled + } + highlight: { + opacity: 0-1 + } + } + } + +More detail and specific examples can be found in the included HTML file. + +*/ + +import { i18n } from '@kbn/i18n'; + +(function($) { + // Maximum redraw attempts when fitting labels within the plot + + var REDRAW_ATTEMPTS = 10; + + // Factor by which to shrink the pie when fitting labels within the plot + + var REDRAW_SHRINK = 0.95; + + function init(plot) { + + var canvas = null, + target = null, + options = null, + maxRadius = null, + centerLeft = null, + centerTop = null, + processed = false, + ctx = null; + + // interactive variables + + var highlights = []; + + // add hook to determine if pie plugin in enabled, and then perform necessary operations + + plot.hooks.processOptions.push(function(plot, options) { + if (options.series.pie.show) { + + options.grid.show = false; + + // set labels.show + + if (options.series.pie.label.show == "auto") { + if (options.legend.show) { + options.series.pie.label.show = false; + } else { + options.series.pie.label.show = true; + } + } + + // set radius + + if (options.series.pie.radius == "auto") { + if (options.series.pie.label.show) { + options.series.pie.radius = 3/4; + } else { + options.series.pie.radius = 1; + } + } + + // ensure sane tilt + + if (options.series.pie.tilt > 1) { + options.series.pie.tilt = 1; + } else if (options.series.pie.tilt < 0) { + options.series.pie.tilt = 0; + } + } + }); + + plot.hooks.bindEvents.push(function(plot, eventHolder) { + var options = plot.getOptions(); + if (options.series.pie.show) { + if (options.grid.hoverable) { + eventHolder.unbind("mousemove").mousemove(onMouseMove); + } + if (options.grid.clickable) { + eventHolder.unbind("click").click(onClick); + } + } + }); + + plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { + var options = plot.getOptions(); + if (options.series.pie.show) { + processDatapoints(plot, series, data, datapoints); + } + }); + + plot.hooks.drawOverlay.push(function(plot, octx) { + var options = plot.getOptions(); + if (options.series.pie.show) { + drawOverlay(plot, octx); + } + }); + + plot.hooks.draw.push(function(plot, newCtx) { + var options = plot.getOptions(); + if (options.series.pie.show) { + draw(plot, newCtx); + } + }); + + function processDatapoints(plot, series, datapoints) { + if (!processed) { + processed = true; + canvas = plot.getCanvas(); + target = $(canvas).parent(); + options = plot.getOptions(); + plot.setData(combine(plot.getData())); + } + } + + function combine(data) { + + var total = 0, + combined = 0, + numCombined = 0, + color = options.series.pie.combine.color, + newdata = []; + + // Fix up the raw data from Flot, ensuring the data is numeric + + for (var i = 0; i < data.length; ++i) { + + var value = data[i].data; + + // If the data is an array, we'll assume that it's a standard + // Flot x-y pair, and are concerned only with the second value. + + // Note how we use the original array, rather than creating a + // new one; this is more efficient and preserves any extra data + // that the user may have stored in higher indexes. + + if ($.isArray(value) && value.length == 1) { + value = value[0]; + } + + if ($.isArray(value)) { + // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 + if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { + value[1] = +value[1]; + } else { + value[1] = 0; + } + } else if (!isNaN(parseFloat(value)) && isFinite(value)) { + value = [1, +value]; + } else { + value = [1, 0]; + } + + data[i].data = [value]; + } + + // Sum up all the slices, so we can calculate percentages for each + + for (var i = 0; i < data.length; ++i) { + total += data[i].data[0][1]; + } + + // Count the number of slices with percentages below the combine + // threshold; if it turns out to be just one, we won't combine. + + for (var i = 0; i < data.length; ++i) { + var value = data[i].data[0][1]; + if (value / total <= options.series.pie.combine.threshold) { + combined += value; + numCombined++; + if (!color) { + color = data[i].color; + } + } + } + + for (var i = 0; i < data.length; ++i) { + var value = data[i].data[0][1]; + if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { + newdata.push( + $.extend(data[i], { /* extend to allow keeping all other original data values + and using them e.g. in labelFormatter. */ + data: [[1, value]], + color: data[i].color, + label: data[i].label, + angle: value * Math.PI * 2 / total, + percent: value / (total / 100) + }) + ); + } + } + + if (numCombined > 1) { + newdata.push({ + data: [[1, combined]], + color: color, + label: options.series.pie.combine.label, + angle: combined * Math.PI * 2 / total, + percent: combined / (total / 100) + }); + } + + return newdata; + } + + function draw(plot, newCtx) { + + if (!target) { + return; // if no series were passed + } + + var canvasWidth = plot.getPlaceholder().width(), + canvasHeight = plot.getPlaceholder().height(), + legendWidth = target.children().filter(".legend").children().width() || 0; + + ctx = newCtx; + + // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! + + // When combining smaller slices into an 'other' slice, we need to + // add a new series. Since Flot gives plugins no way to modify the + // list of series, the pie plugin uses a hack where the first call + // to processDatapoints results in a call to setData with the new + // list of series, then subsequent processDatapoints do nothing. + + // The plugin-global 'processed' flag is used to control this hack; + // it starts out false, and is set to true after the first call to + // processDatapoints. + + // Unfortunately this turns future setData calls into no-ops; they + // call processDatapoints, the flag is true, and nothing happens. + + // To fix this we'll set the flag back to false here in draw, when + // all series have been processed, so the next sequence of calls to + // processDatapoints once again starts out with a slice-combine. + // This is really a hack; in 0.9 we need to give plugins a proper + // way to modify series before any processing begins. + + processed = false; + + // calculate maximum radius and center point + + maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; + centerTop = canvasHeight / 2 + options.series.pie.offset.top; + centerLeft = canvasWidth / 2; + + if (options.series.pie.offset.left == "auto") { + if (options.legend.position.match("w")) { + centerLeft += legendWidth / 2; + } else { + centerLeft -= legendWidth / 2; + } + if (centerLeft < maxRadius) { + centerLeft = maxRadius; + } else if (centerLeft > canvasWidth - maxRadius) { + centerLeft = canvasWidth - maxRadius; + } + } else { + centerLeft += options.series.pie.offset.left; + } + + var slices = plot.getData(), + attempts = 0; + + // Keep shrinking the pie's radius until drawPie returns true, + // indicating that all the labels fit, or we try too many times. + + do { + if (attempts > 0) { + maxRadius *= REDRAW_SHRINK; + } + attempts += 1; + clear(); + if (options.series.pie.tilt <= 0.8) { + drawShadow(); + } + } while (!drawPie() && attempts < REDRAW_ATTEMPTS) + + if (attempts >= REDRAW_ATTEMPTS) { + clear(); + const errorMessage = i18n.translate('xpack.monitoring.pie.unableToDrawLabelsInsideCanvasErrorMessage', { + defaultMessage: 'Could not draw pie with labels contained inside canvas', + }); + target.prepend(`
${errorMessage}
`); + } + + if (plot.setSeries && plot.insertLegend) { + plot.setSeries(slices); + plot.insertLegend(); + } + + // we're actually done at this point, just defining internal functions at this point + + function clear() { + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + target.children().filter(".pieLabel, .pieLabelBackground").remove(); + } + + function drawShadow() { + + var shadowLeft = options.series.pie.shadow.left; + var shadowTop = options.series.pie.shadow.top; + var edge = 10; + var alpha = options.series.pie.shadow.alpha; + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { + return; // shadow would be outside canvas, so don't draw it + } + + ctx.save(); + ctx.translate(shadowLeft,shadowTop); + ctx.globalAlpha = alpha; + ctx.fillStyle = "#000"; + + // center and rotate to starting position + + ctx.translate(centerLeft,centerTop); + ctx.scale(1, options.series.pie.tilt); + + //radius -= edge; + + for (var i = 1; i <= edge; i++) { + ctx.beginPath(); + ctx.arc(0, 0, radius, 0, Math.PI * 2, false); + ctx.fill(); + radius -= i; + } + + ctx.restore(); + } + + function drawPie() { + + var startAngle = Math.PI * options.series.pie.startAngle; + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + // center and rotate to starting position + + ctx.save(); + ctx.translate(centerLeft,centerTop); + ctx.scale(1, options.series.pie.tilt); + //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera + + // draw slices + + ctx.save(); + var currentAngle = startAngle; + for (var i = 0; i < slices.length; ++i) { + slices[i].startAngle = currentAngle; + drawSlice(slices[i].angle, slices[i].color, true); + } + ctx.restore(); + + // draw slice outlines + + if (options.series.pie.stroke.width > 0) { + ctx.save(); + ctx.lineWidth = options.series.pie.stroke.width; + currentAngle = startAngle; + for (var i = 0; i < slices.length; ++i) { + drawSlice(slices[i].angle, options.series.pie.stroke.color, false); + } + ctx.restore(); + } + + // draw donut hole + + drawDonutHole(ctx); + + ctx.restore(); + + // Draw the labels, returning true if they fit within the plot + + if (options.series.pie.label.show) { + return drawLabels(); + } else return true; + + function drawSlice(angle, color, fill) { + + if (angle <= 0 || isNaN(angle)) { + return; + } + + if (fill) { + ctx.fillStyle = color; + } else { + ctx.strokeStyle = color; + ctx.lineJoin = "round"; + } + + ctx.beginPath(); + if (Math.abs(angle - Math.PI * 2) > 0.000000001) { + ctx.moveTo(0, 0); // Center of the pie + } + + //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera + ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); + ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); + ctx.closePath(); + //ctx.rotate(angle); // This doesn't work properly in Opera + currentAngle += angle; + + if (fill) { + ctx.fill(); + } else { + ctx.stroke(); + } + } + + function drawLabels() { + + var currentAngle = startAngle; + var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; + + for (var i = 0; i < slices.length; ++i) { + if (slices[i].percent >= options.series.pie.label.threshold * 100) { + if (!drawLabel(slices[i], currentAngle, i)) { + return false; + } + } + currentAngle += slices[i].angle; + } + + return true; + + function drawLabel(slice, startAngle, index) { + + if (slice.data[0][1] == 0) { + return true; + } + + // format label text + + var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; + + if (lf) { + text = lf(slice.label, slice); + } else { + text = slice.label; + } + + if (plf) { + text = plf(text, slice); + } + + var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; + var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); + var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; + + var html = "" + text + ""; + target.append(html); + + var label = target.children("#pieLabel" + index); + var labelTop = (y - label.height() / 2); + var labelLeft = (x - label.width() / 2); + + label.css("top", labelTop); + label.css("left", labelLeft); + + // check to make sure that the label is not outside the canvas + + if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { + return false; + } + + if (options.series.pie.label.background.opacity != 0) { + + // put in the transparent background separately to avoid blended labels and label boxes + + var c = options.series.pie.label.background.color; + + if (c == null) { + c = slice.color; + } + + var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; + $("
") + .css("opacity", options.series.pie.label.background.opacity) + .insertBefore(label); + } + + return true; + } // end individual label function + } // end drawLabels function + } // end drawPie function + } // end draw function + + // Placed here because it needs to be accessed from multiple locations + + function drawDonutHole(layer) { + if (options.series.pie.innerRadius > 0) { + + // subtract the center + + layer.save(); + var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; + layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color + layer.beginPath(); + layer.fillStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.fill(); + layer.closePath(); + layer.restore(); + + // add inner stroke + + layer.save(); + layer.beginPath(); + layer.strokeStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.stroke(); + layer.closePath(); + layer.restore(); + + // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. + } + } + + //-- Additional Interactive related functions -- + + function isPointInPoly(poly, pt) { + for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) + ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) + && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) + && (c = !c); + return c; + } + + function findNearbySlice(mouseX, mouseY) { + + var slices = plot.getData(), + options = plot.getOptions(), + radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, + x, y; + + for (var i = 0; i < slices.length; ++i) { + + var s = slices[i]; + + if (s.pie.show) { + + ctx.save(); + ctx.beginPath(); + ctx.moveTo(0, 0); // Center of the pie + //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. + ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); + ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); + ctx.closePath(); + x = mouseX - centerLeft; + y = mouseY - centerTop; + + if (ctx.isPointInPath) { + if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i + }; + } + } else { + + // excanvas for IE doesn;t support isPointInPath, this is a workaround. + + var p1X = radius * Math.cos(s.startAngle), + p1Y = radius * Math.sin(s.startAngle), + p2X = radius * Math.cos(s.startAngle + s.angle / 4), + p2Y = radius * Math.sin(s.startAngle + s.angle / 4), + p3X = radius * Math.cos(s.startAngle + s.angle / 2), + p3Y = radius * Math.sin(s.startAngle + s.angle / 2), + p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), + p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), + p5X = radius * Math.cos(s.startAngle + s.angle), + p5Y = radius * Math.sin(s.startAngle + s.angle), + arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], + arrPoint = [x, y]; + + // TODO: perhaps do some mathematical trickery here with the Y-coordinate to compensate for pie tilt? + + if (isPointInPoly(arrPoly, arrPoint)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i + }; + } + } + + ctx.restore(); + } + } + + return null; + } + + function onMouseMove(e) { + triggerClickHoverEvent("plothover", e); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e); + } + + // trigger click or hover event (they send the same parameters so we share their code) + + function triggerClickHoverEvent(eventname, e) { + + var offset = plot.offset(); + var canvasX = parseInt(e.pageX - offset.left); + var canvasY = parseInt(e.pageY - offset.top); + var item = findNearbySlice(canvasX, canvasY); + + if (options.grid.autoHighlight) { + + // clear auto-highlights + + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto == eventname && !(item && h.series == item.series)) { + unhighlight(h.series); + } + } + } + + // highlight the slice + + if (item) { + highlight(item.series, eventname); + } + + // trigger any hover bind events + + var pos = { pageX: e.pageX, pageY: e.pageY }; + target.trigger(eventname, [pos, item]); + } + + function highlight(s, auto) { + //if (typeof s == "number") { + // s = series[s]; + //} + + var i = indexOfHighlight(s); + + if (i == -1) { + highlights.push({ series: s, auto: auto }); + plot.triggerRedrawOverlay(); + } else if (!auto) { + highlights[i].auto = false; + } + } + + function unhighlight(s) { + if (s == null) { + highlights = []; + plot.triggerRedrawOverlay(); + } + + //if (typeof s == "number") { + // s = series[s]; + //} + + var i = indexOfHighlight(s); + + if (i != -1) { + highlights.splice(i, 1); + plot.triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s) + return i; + } + return -1; + } + + function drawOverlay(plot, octx) { + + var options = plot.getOptions(); + + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + octx.save(); + octx.translate(centerLeft, centerTop); + octx.scale(1, options.series.pie.tilt); + + for (var i = 0; i < highlights.length; ++i) { + drawHighlight(highlights[i].series); + } + + drawDonutHole(octx); + + octx.restore(); + + function drawHighlight(series) { + + if (series.angle <= 0 || isNaN(series.angle)) { + return; + } + + //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); + octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor + octx.beginPath(); + if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { + octx.moveTo(0, 0); // Center of the pie + } + octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); + octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); + octx.closePath(); + octx.fill(); + } + } + } // end init (plugin body) + + // define pie specific options and their default values + + var options = { + series: { + pie: { + show: false, + radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) + innerRadius: 0, /* for donut */ + startAngle: 3/2, + tilt: 1, + shadow: { + left: 5, // shadow left offset + top: 15, // shadow top offset + alpha: 0.02 // shadow alpha + }, + offset: { + top: 0, + left: "auto" + }, + stroke: { + color: "#fff", + width: 1 + }, + label: { + show: "auto", + formatter: function(label, slice) { + return "
" + label + "
" + Math.round(slice.percent) + "%
"; + }, // formatter function + radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) + background: { + color: null, + opacity: 0 + }, + threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) + }, + combine: { + threshold: -1, // percentage at which to combine little slices into one larger slice + color: null, // color to give the new slice (auto-generated if null) + label: "Other" // label to give the new slice + }, + highlight: { + //color: "#fff", // will add this functionality once parseColor is available + opacity: 0.5 + } + } + } + }; + + $.plot.plugins.push({ + init: init, + options: options, + name: "pie", + version: "1.1" + }); + +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.resize.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.resize.js new file mode 100644 index 0000000000000..8a626dda0addb --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.resize.js @@ -0,0 +1,59 @@ +/* Flot plugin for automatically redrawing plots as the placeholder resizes. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +It works by listening for changes on the placeholder div (through the jQuery +resize event plugin) - if the size changes, it will redraw the plot. + +There are no options. If you need to disable the plugin for some plots, you +can just fix the size of their placeholders. + +*/ + +/* Inline dependency: + * jQuery resize event - v1.1 - 3/14/2010 + * http://benalman.com/projects/jquery-resize-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this); + +(function ($) { + var options = { }; // no options + + function init(plot) { + function onResize() { + var placeholder = plot.getPlaceholder(); + + // somebody might have hidden us and we can't plot + // when we don't have the dimensions + if (placeholder.width() == 0 || placeholder.height() == 0) + return; + + plot.resize(); + plot.setupGrid(); + plot.draw(); + } + + function bindEvents(plot, eventHolder) { + plot.getPlaceholder().resize(onResize); + } + + function shutdown(plot, eventHolder) { + plot.getPlaceholder().unbind("resize", onResize); + } + + plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'resize', + version: '1.0' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.selection.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.selection.js new file mode 100644 index 0000000000000..c8707b30f4e6f --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.selection.js @@ -0,0 +1,360 @@ +/* Flot plugin for selecting regions of a plot. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin supports these options: + +selection: { + mode: null or "x" or "y" or "xy", + color: color, + shape: "round" or "miter" or "bevel", + minSize: number of pixels +} + +Selection support is enabled by setting the mode to one of "x", "y" or "xy". +In "x" mode, the user will only be able to specify the x range, similarly for +"y" mode. For "xy", the selection becomes a rectangle where both ranges can be +specified. "color" is color of the selection (if you need to change the color +later on, you can get to it with plot.getOptions().selection.color). "shape" +is the shape of the corners of the selection. + +"minSize" is the minimum size a selection can be in pixels. This value can +be customized to determine the smallest size a selection can be and still +have the selection rectangle be displayed. When customizing this value, the +fact that it refers to pixels, not axis units must be taken into account. +Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 +minute, setting "minSize" to 1 will not make the minimum selection size 1 +minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent +"plotunselected" events from being fired when the user clicks the mouse without +dragging. + +When selection support is enabled, a "plotselected" event will be emitted on +the DOM element you passed into the plot function. The event handler gets a +parameter with the ranges selected on the axes, like this: + + placeholder.bind( "plotselected", function( event, ranges ) { + alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) + // similar for yaxis - with multiple axes, the extra ones are in + // x2axis, x3axis, ... + }); + +The "plotselected" event is only fired when the user has finished making the +selection. A "plotselecting" event is fired during the process with the same +parameters as the "plotselected" event, in case you want to know what's +happening while it's happening, + +A "plotunselected" event with no arguments is emitted when the user clicks the +mouse to remove the selection. As stated above, setting "minSize" to 0 will +destroy this behavior. + +The plugin also adds the following methods to the plot object: + +- setSelection( ranges, preventEvent ) + + Set the selection rectangle. The passed in ranges is on the same form as + returned in the "plotselected" event. If the selection mode is "x", you + should put in either an xaxis range, if the mode is "y" you need to put in + an yaxis range and both xaxis and yaxis if the selection mode is "xy", like + this: + + setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); + + setSelection will trigger the "plotselected" event when called. If you don't + want that to happen, e.g. if you're inside a "plotselected" handler, pass + true as the second parameter. If you are using multiple axes, you can + specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of + xaxis, the plugin picks the first one it sees. + +- clearSelection( preventEvent ) + + Clear the selection rectangle. Pass in true to avoid getting a + "plotunselected" event. + +- getSelection() + + Returns the current selection in the same format as the "plotselected" + event. If there's currently no selection, the function returns null. + +*/ + +(function ($) { + function init(plot) { + var selection = { + first: { x: -1, y: -1}, second: { x: -1, y: -1}, + show: false, + active: false + }; + + // FIXME: The drag handling implemented here should be + // abstracted out, there's some similar code from a library in + // the navigation plugin, this should be massaged a bit to fit + // the Flot cases here better and reused. Doing this would + // make this plugin much slimmer. + var savedhandlers = {}; + + var mouseUpHandler = null; + + function onMouseMove(e) { + if (selection.active) { + updateSelection(e); + + plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); + } + } + + function onMouseDown(e) { + if (e.which != 1) // only accept left-click + return; + + // cancel out any text selections + document.body.focus(); + + // prevent text selection and drag in old-school browsers + if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { + savedhandlers.onselectstart = document.onselectstart; + document.onselectstart = function () { return false; }; + } + if (document.ondrag !== undefined && savedhandlers.ondrag == null) { + savedhandlers.ondrag = document.ondrag; + document.ondrag = function () { return false; }; + } + + setSelectionPos(selection.first, e); + + selection.active = true; + + // this is a bit silly, but we have to use a closure to be + // able to whack the same handler again + mouseUpHandler = function (e) { onMouseUp(e); }; + + $(document).one("mouseup", mouseUpHandler); + } + + function onMouseUp(e) { + mouseUpHandler = null; + + // revert drag stuff for old-school browsers + if (document.onselectstart !== undefined) + document.onselectstart = savedhandlers.onselectstart; + if (document.ondrag !== undefined) + document.ondrag = savedhandlers.ondrag; + + // no more dragging + selection.active = false; + updateSelection(e); + + if (selectionIsSane()) + triggerSelectedEvent(); + else { + // this counts as a clear + plot.getPlaceholder().trigger("plotunselected", [ ]); + plot.getPlaceholder().trigger("plotselecting", [ null ]); + } + + return false; + } + + function getSelection() { + if (!selectionIsSane()) + return null; + + if (!selection.show) return null; + + var r = {}, c1 = selection.first, c2 = selection.second; + $.each(plot.getAxes(), function (name, axis) { + if (axis.used) { + var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); + r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; + } + }); + return r; + } + + function triggerSelectedEvent() { + var r = getSelection(); + + plot.getPlaceholder().trigger("plotselected", [ r ]); + + // backwards-compat stuff, to be removed in future + if (r.xaxis && r.yaxis) + plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); + } + + function clamp(min, value, max) { + return value < min ? min: (value > max ? max: value); + } + + function setSelectionPos(pos, e) { + var o = plot.getOptions(); + var offset = plot.getPlaceholder().offset(); + var plotOffset = plot.getPlotOffset(); + pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); + pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); + + if (o.selection.mode == "y") + pos.x = pos == selection.first ? 0 : plot.width(); + + if (o.selection.mode == "x") + pos.y = pos == selection.first ? 0 : plot.height(); + } + + function updateSelection(pos) { + if (pos.pageX == null) + return; + + setSelectionPos(selection.second, pos); + if (selectionIsSane()) { + selection.show = true; + plot.triggerRedrawOverlay(); + } + else + clearSelection(true); + } + + function clearSelection(preventEvent) { + if (selection.show) { + selection.show = false; + plot.triggerRedrawOverlay(); + if (!preventEvent) + plot.getPlaceholder().trigger("plotunselected", [ ]); + } + } + + // function taken from markings support in Flot + function extractRange(ranges, coord) { + var axis, from, to, key, axes = plot.getAxes(); + + for (var k in axes) { + axis = axes[k]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function setSelection(ranges, preventEvent) { + var axis, range, o = plot.getOptions(); + + if (o.selection.mode == "y") { + selection.first.x = 0; + selection.second.x = plot.width(); + } + else { + range = extractRange(ranges, "x"); + + selection.first.x = range.axis.p2c(range.from); + selection.second.x = range.axis.p2c(range.to); + } + + if (o.selection.mode == "x") { + selection.first.y = 0; + selection.second.y = plot.height(); + } + else { + range = extractRange(ranges, "y"); + + selection.first.y = range.axis.p2c(range.from); + selection.second.y = range.axis.p2c(range.to); + } + + selection.show = true; + plot.triggerRedrawOverlay(); + if (!preventEvent && selectionIsSane()) + triggerSelectedEvent(); + } + + function selectionIsSane() { + var minSize = plot.getOptions().selection.minSize; + return Math.abs(selection.second.x - selection.first.x) >= minSize && + Math.abs(selection.second.y - selection.first.y) >= minSize; + } + + plot.clearSelection = clearSelection; + plot.setSelection = setSelection; + plot.getSelection = getSelection; + + plot.hooks.bindEvents.push(function(plot, eventHolder) { + var o = plot.getOptions(); + if (o.selection.mode != null) { + eventHolder.mousemove(onMouseMove); + eventHolder.mousedown(onMouseDown); + } + }); + + + plot.hooks.drawOverlay.push(function (plot, ctx) { + // draw selection + if (selection.show && selectionIsSane()) { + var plotOffset = plot.getPlotOffset(); + var o = plot.getOptions(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var c = $.color.parse(o.selection.color); + + ctx.strokeStyle = c.scale('a', 0.8).toString(); + ctx.lineWidth = 1; + ctx.lineJoin = o.selection.shape; + ctx.fillStyle = c.scale('a', 0.4).toString(); + + var x = Math.min(selection.first.x, selection.second.x) + 0.5, + y = Math.min(selection.first.y, selection.second.y) + 0.5, + w = Math.abs(selection.second.x - selection.first.x) - 1, + h = Math.abs(selection.second.y - selection.first.y) - 1; + + ctx.fillRect(x, y, w, h); + ctx.strokeRect(x, y, w, h); + + ctx.restore(); + } + }); + + plot.hooks.shutdown.push(function (plot, eventHolder) { + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mousedown", onMouseDown); + + if (mouseUpHandler) + $(document).unbind("mouseup", mouseUpHandler); + }); + + } + + $.plot.plugins.push({ + init: init, + options: { + selection: { + mode: null, // one of null, "x", "y" or "xy" + color: "#e8cfac", + shape: "round", // one of "round", "miter", or "bevel" + minSize: 5 // minimum number of pixels + } + }, + name: 'selection', + version: '1.1' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.stack.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.stack.js new file mode 100644 index 0000000000000..0d91c0f3c0160 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.stack.js @@ -0,0 +1,188 @@ +/* Flot plugin for stacking data sets rather than overlaying them. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes the data is sorted on x (or y if stacking horizontally). +For line charts, it is assumed that if a line has an undefined gap (from a +null point), then the line above it should have the same gap - insert zeros +instead of "null" if you want another behaviour. This also holds for the start +and end of the chart. Note that stacking a mix of positive and negative values +in most instances doesn't make sense (so it looks weird). + +Two or more series are stacked when their "stack" attribute is set to the same +key (which can be any number or string or just "true"). To specify the default +stack, you can set the stack option like this: + + series: { + stack: null/false, true, or a key (number/string) + } + +You can also specify it for a single series, like this: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + stack: true + }]) + +The stacking order is determined by the order of the data series in the array +(later series end up on top of the previous). + +Internally, the plugin modifies the datapoints in each series, adding an +offset to the y value. For line series, extra data points are inserted through +interpolation. If there's a second y value, it's also adjusted (e.g for bar +charts or filled areas). + +*/ + +(function ($) { + var options = { + series: { stack: null } // or number/string + }; + + function init(plot) { + function findMatchingSeries(s, allseries) { + var res = null; + for (var i = 0; i < allseries.length; ++i) { + if (s == allseries[i]) + break; + + if (allseries[i].stack == s.stack) + res = allseries[i]; + } + + return res; + } + + function stackData(plot, s, datapoints) { + if (s.stack == null || s.stack === false) + return; + + var other = findMatchingSeries(s, plot.getData()); + if (!other) + return; + + var ps = datapoints.pointsize, + points = datapoints.points, + otherps = other.datapoints.pointsize, + otherpoints = other.datapoints.points, + newpoints = [], + px, py, intery, qx, qy, bottom, + withlines = s.lines.show, + horizontal = s.bars.horizontal, + withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), + withsteps = withlines && s.lines.steps, + fromgap = true, + keyOffset = horizontal ? 1 : 0, + accumulateOffset = horizontal ? 0 : 1, + i = 0, j = 0, l, m; + + while (true) { + if (i >= points.length) + break; + + l = newpoints.length; + + if (points[i] == null) { + // copy gaps + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + i += ps; + } + else if (j >= otherpoints.length) { + // for lines, we can't use the rest of the points + if (!withlines) { + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + } + i += ps; + } + else if (otherpoints[j] == null) { + // oops, got a gap + for (m = 0; m < ps; ++m) + newpoints.push(null); + fromgap = true; + j += otherps; + } + else { + // cases where we actually got two points + px = points[i + keyOffset]; + py = points[i + accumulateOffset]; + qx = otherpoints[j + keyOffset]; + qy = otherpoints[j + accumulateOffset]; + bottom = 0; + + if (px == qx) { + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + + newpoints[l + accumulateOffset] += qy; + bottom = qy; + + i += ps; + j += otherps; + } + else if (px > qx) { + // we got past point below, might need to + // insert interpolated extra point + if (withlines && i > 0 && points[i - ps] != null) { + intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); + newpoints.push(qx); + newpoints.push(intery + qy); + for (m = 2; m < ps; ++m) + newpoints.push(points[i + m]); + bottom = qy; + } + + j += otherps; + } + else { // px < qx + if (fromgap && withlines) { + // if we come from a gap, we just skip this point + i += ps; + continue; + } + + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + + // we might be able to interpolate a point below, + // this can give us a better y + if (withlines && j > 0 && otherpoints[j - otherps] != null) + bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); + + newpoints[l + accumulateOffset] += bottom; + + i += ps; + } + + fromgap = false; + + if (l != newpoints.length && withbottom) + newpoints[l + 2] += bottom; + } + + // maintain the line steps invariant + if (withsteps && l != newpoints.length && l > 0 + && newpoints[l] != null + && newpoints[l] != newpoints[l - ps] + && newpoints[l + 1] != newpoints[l - ps + 1]) { + for (m = 0; m < ps; ++m) + newpoints[l + ps + m] = newpoints[l + m]; + newpoints[l + 1] = newpoints[l - ps + 1]; + } + } + + datapoints.points = newpoints; + } + + plot.hooks.processDatapoints.push(stackData); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'stack', + version: '1.2' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.symbol.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.symbol.js new file mode 100644 index 0000000000000..79f634971b6fa --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.symbol.js @@ -0,0 +1,71 @@ +/* Flot plugin that adds some extra symbols for plotting points. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The symbols are accessed as strings through the standard symbol options: + + series: { + points: { + symbol: "square" // or "diamond", "triangle", "cross" + } + } + +*/ + +(function ($) { + function processRawData(plot, series, datapoints) { + // we normalize the area of each symbol so it is approximately the + // same as a circle of the given radius + + var handlers = { + square: function (ctx, x, y, radius, shadow) { + // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.rect(x - size, y - size, size + size, size + size); + }, + diamond: function (ctx, x, y, radius, shadow) { + // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) + var size = radius * Math.sqrt(Math.PI / 2); + ctx.moveTo(x - size, y); + ctx.lineTo(x, y - size); + ctx.lineTo(x + size, y); + ctx.lineTo(x, y + size); + ctx.lineTo(x - size, y); + }, + triangle: function (ctx, x, y, radius, shadow) { + // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) + var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); + var height = size * Math.sin(Math.PI / 3); + ctx.moveTo(x - size/2, y + height/2); + ctx.lineTo(x + size/2, y + height/2); + if (!shadow) { + ctx.lineTo(x, y - height/2); + ctx.lineTo(x - size/2, y + height/2); + } + }, + cross: function (ctx, x, y, radius, shadow) { + // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.moveTo(x - size, y - size); + ctx.lineTo(x + size, y + size); + ctx.moveTo(x - size, y + size); + ctx.lineTo(x + size, y - size); + } + }; + + var s = series.points.symbol; + if (handlers[s]) + series.points.symbol = handlers[s]; + } + + function init(plot) { + plot.hooks.processDatapoints.push(processRawData); + } + + $.plot.plugins.push({ + init: init, + name: 'symbols', + version: '1.0' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.threshold.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.threshold.js new file mode 100644 index 0000000000000..8c99c401d87e5 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.threshold.js @@ -0,0 +1,142 @@ +/* Flot plugin for thresholding data. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin supports these options: + + series: { + threshold: { + below: number + color: colorspec + } + } + +It can also be applied to a single series, like this: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + threshold: { ... } + }]) + +An array can be passed for multiple thresholding, like this: + + threshold: [{ + below: number1 + color: color1 + },{ + below: number2 + color: color2 + }] + +These multiple threshold objects can be passed in any order since they are +sorted by the processing function. + +The data points below "below" are drawn with the specified color. This makes +it easy to mark points below 0, e.g. for budget data. + +Internally, the plugin works by splitting the data into two series, above and +below the threshold. The extra series below the threshold will have its label +cleared and the special "originSeries" attribute set to the original series. +You may need to check for this in hover events. + +*/ + +(function ($) { + var options = { + series: { threshold: null } // or { below: number, color: color spec} + }; + + function init(plot) { + function thresholdData(plot, s, datapoints, below, color) { + var ps = datapoints.pointsize, i, x, y, p, prevp, + thresholded = $.extend({}, s); // note: shallow copy + + thresholded.datapoints = { points: [], pointsize: ps, format: datapoints.format }; + thresholded.label = null; + thresholded.color = color; + thresholded.threshold = null; + thresholded.originSeries = s; + thresholded.data = []; + + var origpoints = datapoints.points, + addCrossingPoints = s.lines.show; + + var threspoints = []; + var newpoints = []; + var m; + + for (i = 0; i < origpoints.length; i += ps) { + x = origpoints[i]; + y = origpoints[i + 1]; + + prevp = p; + if (y < below) + p = threspoints; + else + p = newpoints; + + if (addCrossingPoints && prevp != p && x != null + && i > 0 && origpoints[i - ps] != null) { + var interx = x + (below - y) * (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]); + prevp.push(interx); + prevp.push(below); + for (m = 2; m < ps; ++m) + prevp.push(origpoints[i + m]); + + p.push(null); // start new segment + p.push(null); + for (m = 2; m < ps; ++m) + p.push(origpoints[i + m]); + p.push(interx); + p.push(below); + for (m = 2; m < ps; ++m) + p.push(origpoints[i + m]); + } + + p.push(x); + p.push(y); + for (m = 2; m < ps; ++m) + p.push(origpoints[i + m]); + } + + datapoints.points = newpoints; + thresholded.datapoints.points = threspoints; + + if (thresholded.datapoints.points.length > 0) { + var origIndex = $.inArray(s, plot.getData()); + // Insert newly-generated series right after original one (to prevent it from becoming top-most) + plot.getData().splice(origIndex + 1, 0, thresholded); + } + + // FIXME: there are probably some edge cases left in bars + } + + function processThresholds(plot, s, datapoints) { + if (!s.threshold) + return; + + if (s.threshold instanceof Array) { + s.threshold.sort(function(a, b) { + return a.below - b.below; + }); + + $(s.threshold).each(function(i, th) { + thresholdData(plot, s, datapoints, th.below, th.color); + }); + } + else { + thresholdData(plot, s, datapoints, s.threshold.below, s.threshold.color); + } + } + + plot.hooks.processDatapoints.push(processThresholds); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'threshold', + version: '1.2' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js new file mode 100644 index 0000000000000..991e87d364e8a --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js @@ -0,0 +1,473 @@ +/* Pretty handling of time axes. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Set axis.mode to "time" to enable. See the section "Time series data" in +API.txt for details. + +*/ + +import { i18n } from '@kbn/i18n'; + +(function($) { + + var options = { + xaxis: { + timezone: null, // "browser" for local to the client or timezone for timezone-js + timeformat: null, // format string to use + twelveHourClock: false, // 12 or 24 time in time mode + monthNames: null // list of names of months + } + }; + + // round to nearby lower multiple of base + + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + + // Returns a string with the date d formatted according to fmt. + // A subset of the Open Group's strftime format is supported. + + function formatDate(d, fmt, monthNames, dayNames) { + + if (typeof d.strftime == "function") { + return d.strftime(fmt); + } + + var leftPad = function(n, pad) { + n = "" + n; + pad = "" + (pad == null ? "0" : pad); + return n.length == 1 ? pad + n : n; + }; + + var r = []; + var escape = false; + var hours = d.getHours(); + var isAM = hours < 12; + + if (monthNames == null) { + monthNames = [ + i18n.translate('xpack.monitoring.janLabel', { + defaultMessage: 'Jan', + }), i18n.translate('xpack.monitoring.febLabel', { + defaultMessage: 'Feb', + }), i18n.translate('xpack.monitoring.marLabel', { + defaultMessage: 'Mar', + }), i18n.translate('xpack.monitoring.aprLabel', { + defaultMessage: 'Apr', + }), i18n.translate('xpack.monitoring.mayLabel', { + defaultMessage: 'May', + }), i18n.translate('xpack.monitoring.junLabel', { + defaultMessage: 'Jun', + }), i18n.translate('xpack.monitoring.julLabel', { + defaultMessage: 'Jul', + }), i18n.translate('xpack.monitoring.augLabel', { + defaultMessage: 'Aug', + }), i18n.translate('xpack.monitoring.sepLabel', { + defaultMessage: 'Sep', + }), i18n.translate('xpack.monitoring.octLabel', { + defaultMessage: 'Oct', + }), i18n.translate('xpack.monitoring.novLabel', { + defaultMessage: 'Nov', + }), i18n.translate('xpack.monitoring.decLabel', { + defaultMessage: 'Dec', + })]; + } + + if (dayNames == null) { + dayNames = [i18n.translate('xpack.monitoring.sunLabel', { + defaultMessage: 'Sun', + }), i18n.translate('xpack.monitoring.monLabel', { + defaultMessage: 'Mon', + }), i18n.translate('xpack.monitoring.tueLabel', { + defaultMessage: 'Tue', + }), i18n.translate('xpack.monitoring.wedLabel', { + defaultMessage: 'Wed', + }), i18n.translate('xpack.monitoring.thuLabel', { + defaultMessage: 'Thu', + }), i18n.translate('xpack.monitoring.friLabel', { + defaultMessage: 'Fri', + }), i18n.translate('xpack.monitoring.satLabel', { + defaultMessage: 'Sat', + })]; + } + + var hours12; + + if (hours > 12) { + hours12 = hours - 12; + } else if (hours == 0) { + hours12 = 12; + } else { + hours12 = hours; + } + + for (var i = 0; i < fmt.length; ++i) { + + var c = fmt.charAt(i); + + if (escape) { + switch (c) { + case 'a': c = "" + dayNames[d.getDay()]; break; + case 'b': c = "" + monthNames[d.getMonth()]; break; + case 'd': c = leftPad(d.getDate()); break; + case 'e': c = leftPad(d.getDate(), " "); break; + case 'h': // For back-compat with 0.7; remove in 1.0 + case 'H': c = leftPad(hours); break; + case 'I': c = leftPad(hours12); break; + case 'l': c = leftPad(hours12, " "); break; + case 'm': c = leftPad(d.getMonth() + 1); break; + case 'M': c = leftPad(d.getMinutes()); break; + // quarters not in Open Group's strftime specification + case 'q': + c = "" + (Math.floor(d.getMonth() / 3) + 1); break; + case 'S': c = leftPad(d.getSeconds()); break; + case 'y': c = leftPad(d.getFullYear() % 100); break; + case 'Y': c = "" + d.getFullYear(); break; + case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; + case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; + case 'w': c = "" + d.getDay(); break; + } + r.push(c); + escape = false; + } else { + if (c == "%") { + escape = true; + } else { + r.push(c); + } + } + } + + return r.join(""); + } + + // To have a consistent view of time-based data independent of which time + // zone the client happens to be in we need a date-like object independent + // of time zones. This is done through a wrapper that only calls the UTC + // versions of the accessor methods. + + function makeUtcWrapper(d) { + + function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { + sourceObj[sourceMethod] = function() { + return targetObj[targetMethod].apply(targetObj, arguments); + }; + }; + + var utc = { + date: d + }; + + // support strftime, if found + + if (d.strftime != undefined) { + addProxyMethod(utc, "strftime", d, "strftime"); + } + + addProxyMethod(utc, "getTime", d, "getTime"); + addProxyMethod(utc, "setTime", d, "setTime"); + + var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; + + for (var p = 0; p < props.length; p++) { + addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); + addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); + } + + return utc; + }; + + // select time zone strategy. This returns a date-like object tied to the + // desired timezone + + function dateGenerator(ts, opts) { + if (opts.timezone == "browser") { + return new Date(ts); + } else if (!opts.timezone || opts.timezone == "utc") { + return makeUtcWrapper(new Date(ts)); + } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { + var d = new timezoneJS.Date(); + // timezone-js is fickle, so be sure to set the time zone before + // setting the time. + d.setTimezone(opts.timezone); + d.setTime(ts); + return d; + } else { + return makeUtcWrapper(new Date(ts)); + } + } + + // map of app. size of time units in milliseconds + + var timeUnitSize = { + "second": 1000, + "minute": 60 * 1000, + "hour": 60 * 60 * 1000, + "day": 24 * 60 * 60 * 1000, + "month": 30 * 24 * 60 * 60 * 1000, + "quarter": 3 * 30 * 24 * 60 * 60 * 1000, + "year": 365.2425 * 24 * 60 * 60 * 1000 + }; + + // the allowed tick sizes, after 1 year we use + // an integer algorithm + + var baseSpec = [ + [1, "second"], [2, "second"], [5, "second"], [10, "second"], + [30, "second"], + [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], + [30, "minute"], + [1, "hour"], [2, "hour"], [4, "hour"], + [8, "hour"], [12, "hour"], + [1, "day"], [2, "day"], [3, "day"], + [0.25, "month"], [0.5, "month"], [1, "month"], + [2, "month"] + ]; + + // we don't know which variant(s) we'll need yet, but generating both is + // cheap + + var specMonths = baseSpec.concat([[3, "month"], [6, "month"], + [1, "year"]]); + var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], + [1, "year"]]); + + function init(plot) { + plot.hooks.processOptions.push(function (plot, options) { + $.each(plot.getAxes(), function(axisName, axis) { + + var opts = axis.options; + + if (opts.mode == "time") { + axis.tickGenerator = function(axis) { + + var ticks = []; + var d = dateGenerator(axis.min, opts); + var minSize = 0; + + // make quarter use a possibility if quarters are + // mentioned in either of these options + + var spec = (opts.tickSize && opts.tickSize[1] === + "quarter") || + (opts.minTickSize && opts.minTickSize[1] === + "quarter") ? specQuarters : specMonths; + + if (opts.minTickSize != null) { + if (typeof opts.tickSize == "number") { + minSize = opts.tickSize; + } else { + minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; + } + } + + for (var i = 0; i < spec.length - 1; ++i) { + if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] + + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 + && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { + break; + } + } + + var size = spec[i][0]; + var unit = spec[i][1]; + + // special-case the possibility of several years + + if (unit == "year") { + + // if given a minTickSize in years, just use it, + // ensuring that it's an integer + + if (opts.minTickSize != null && opts.minTickSize[1] == "year") { + size = Math.floor(opts.minTickSize[0]); + } else { + + var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); + var norm = (axis.delta / timeUnitSize.year) / magn; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + } + + // minimum size for years is 1 + + if (size < 1) { + size = 1; + } + } + + axis.tickSize = opts.tickSize || [size, unit]; + var tickSize = axis.tickSize[0]; + unit = axis.tickSize[1]; + + var step = tickSize * timeUnitSize[unit]; + + if (unit == "second") { + d.setSeconds(floorInBase(d.getSeconds(), tickSize)); + } else if (unit == "minute") { + d.setMinutes(floorInBase(d.getMinutes(), tickSize)); + } else if (unit == "hour") { + d.setHours(floorInBase(d.getHours(), tickSize)); + } else if (unit == "month") { + d.setMonth(floorInBase(d.getMonth(), tickSize)); + } else if (unit == "quarter") { + d.setMonth(3 * floorInBase(d.getMonth() / 3, + tickSize)); + } else if (unit == "year") { + d.setFullYear(floorInBase(d.getFullYear(), tickSize)); + } + + // reset smaller components + + d.setMilliseconds(0); + + if (step >= timeUnitSize.minute) { + d.setSeconds(0); + } + if (step >= timeUnitSize.hour) { + d.setMinutes(0); + } + if (step >= timeUnitSize.day) { + d.setHours(0); + } + if (step >= timeUnitSize.day * 4) { + d.setDate(1); + } + if (step >= timeUnitSize.month * 2) { + d.setMonth(floorInBase(d.getMonth(), 3)); + } + if (step >= timeUnitSize.quarter * 2) { + d.setMonth(floorInBase(d.getMonth(), 6)); + } + if (step >= timeUnitSize.year) { + d.setMonth(0); + } + + var carry = 0; + var v = Number.NaN; + var prev; + + do { + + prev = v; + v = d.getTime(); + ticks.push(v); + + if (unit == "month" || unit == "quarter") { + if (tickSize < 1) { + + // a bit complicated - we'll divide the + // month/quarter up but we need to take + // care of fractions so we don't end up in + // the middle of a day + + d.setDate(1); + var start = d.getTime(); + d.setMonth(d.getMonth() + + (unit == "quarter" ? 3 : 1)); + var end = d.getTime(); + d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); + carry = d.getHours(); + d.setHours(0); + } else { + d.setMonth(d.getMonth() + + tickSize * (unit == "quarter" ? 3 : 1)); + } + } else if (unit == "year") { + d.setFullYear(d.getFullYear() + tickSize); + } else { + d.setTime(v + step); + } + } while (v < axis.max && v != prev); + + return ticks; + }; + + axis.tickFormatter = function (v, axis) { + + var d = dateGenerator(v, axis.options); + + // first check global format + + if (opts.timeformat != null) { + return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); + } + + // possibly use quarters if quarters are mentioned in + // any of these places + + var useQuarters = (axis.options.tickSize && + axis.options.tickSize[1] == "quarter") || + (axis.options.minTickSize && + axis.options.minTickSize[1] == "quarter"); + + var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; + var span = axis.max - axis.min; + var suffix = (opts.twelveHourClock) ? " %p" : ""; + var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; + var fmt; + + if (t < timeUnitSize.minute) { + fmt = hourCode + ":%M:%S" + suffix; + } else if (t < timeUnitSize.day) { + if (span < 2 * timeUnitSize.day) { + fmt = hourCode + ":%M" + suffix; + } else { + fmt = "%b %d " + hourCode + ":%M" + suffix; + } + } else if (t < timeUnitSize.month) { + fmt = "%b %d"; + } else if ((useQuarters && t < timeUnitSize.quarter) || + (!useQuarters && t < timeUnitSize.year)) { + if (span < timeUnitSize.year) { + fmt = "%b"; + } else { + fmt = "%b %Y"; + } + } else if (useQuarters && t < timeUnitSize.year) { + if (span < timeUnitSize.year) { + fmt = "Q%q"; + } else { + fmt = "Q%q %Y"; + } + } else { + fmt = "%Y"; + } + + var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); + + return rt; + }; + } + }); + }); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'time', + version: '1.0' + }); + + // Time-axis support used to be in Flot core, which exposed the + // formatDate function on the plot object. Various plugins depend + // on the function, so we need to re-expose it here. + + $.plot.formatDate = formatDate; + $.plot.dateGenerator = dateGenerator; + +})(jQuery); diff --git a/x-pack/legacy/plugins/monitoring/common/index.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/index.js similarity index 76% rename from x-pack/legacy/plugins/monitoring/common/index.js rename to x-pack/plugins/monitoring/public/lib/jquery_flot/index.js index 183396f8f0d72..abf060aca8c08 100644 --- a/x-pack/legacy/plugins/monitoring/common/index.js +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/index.js @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { formatTimestampToDuration } from './format_timestamp_to_duration'; +export { default } from './jquery_flot'; diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/jquery_flot.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/jquery_flot.js new file mode 100644 index 0000000000000..28a4d5f56df15 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/jquery_flot.js @@ -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 $ from 'jquery'; +if (window) { + window.jQuery = $; +} +import './flot-charts/jquery.flot'; + +// load flot plugins +// avoid the `canvas` plugin, it causes blurry fonts +import './flot-charts/jquery.flot.time'; +import './flot-charts/jquery.flot.crosshair'; +import './flot-charts/jquery.flot.selection'; + +export default $; diff --git a/x-pack/legacy/plugins/monitoring/public/lib/logstash/__tests__/pipelines.js b/x-pack/plugins/monitoring/public/lib/logstash/__tests__/pipelines.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/logstash/__tests__/pipelines.js rename to x-pack/plugins/monitoring/public/lib/logstash/__tests__/pipelines.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/logstash/pipelines.js b/x-pack/plugins/monitoring/public/lib/logstash/pipelines.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/logstash/pipelines.js rename to x-pack/plugins/monitoring/public/lib/logstash/pipelines.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/route_init.js b/x-pack/plugins/monitoring/public/lib/route_init.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/lib/route_init.js rename to x-pack/plugins/monitoring/public/lib/route_init.js index 97a55303dae67..2c928334ef63e 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/route_init.js +++ b/x-pack/plugins/monitoring/public/lib/route_init.js @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; +import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { isInSetupMode } from './setup_mode'; import { getClusterFromClusters } from './get_cluster_from_clusters'; diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/plugins/monitoring/public/lib/setup_mode.test.js similarity index 72% rename from x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js rename to x-pack/plugins/monitoring/public/lib/setup_mode.test.js index 765909f0aa251..c54c29df09685 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.test.js @@ -4,12 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - coreMock, - overlayServiceMock, - notificationServiceMock, -} from '../../../../../../src/core/public/mocks'; - let toggleSetupMode; let initSetupModeState; let getSetupModeState; @@ -26,6 +20,14 @@ jest.mock('react-dom', () => ({ render: jest.fn(), })); +jest.mock('../legacy_shims', () => { + return { + Legacy: { + shims: { getAngularInjector: () => ({ get: () => ({ get: () => 'utc' }) }) }, + }, + }; +}); + let data = {}; const injectorModulesMock = { @@ -61,70 +63,10 @@ function waitForSetupModeData(action) { process.nextTick(action); } -function mockFilterManager() { - let subscriber; - let filters = []; - return { - getUpdates$: () => ({ - subscribe: ({ next }) => { - subscriber = next; - return jest.fn(); - }, - }), - setFilters: newFilters => { - filters = newFilters; - subscriber(); - }, - getFilters: () => filters, - removeAll: () => { - filters = []; - subscriber(); - }, - }; -} - -const pluginData = { - query: { - filterManager: mockFilterManager(), - timefilter: { - timefilter: { - getTime: jest.fn(() => ({ from: 'now-1h', to: 'now' })), - setTime: jest.fn(), - }, - }, - }, -}; - -function setModulesAndMocks(isOnCloud = false) { +function setModulesAndMocks() { jest.clearAllMocks().resetModules(); injectorModulesMock.globalState.inSetupMode = false; - jest.doMock('ui/new_platform', () => ({ - npSetup: { - plugins: { - cloud: isOnCloud ? { cloudId: 'test', isCloudEnabled: true } : {}, - uiActions: { - registerAction: jest.fn(), - attachAction: jest.fn(), - }, - }, - core: { - ...coreMock.createSetup(), - notifications: notificationServiceMock.createStartContract(), - }, - }, - npStart: { - plugins: { - data: pluginData, - navigation: { ui: {} }, - }, - core: { - ...coreMock.createStart(), - overlays: overlayServiceMock.createStartContract(), - }, - }, - })); - const setupMode = require('./setup_mode'); toggleSetupMode = setupMode.toggleSetupMode; initSetupModeState = setupMode.initSetupModeState; @@ -179,37 +121,15 @@ describe('setup_mode', () => { data = {}; }); - it('should not fetch data if on cloud', async done => { - const addDanger = jest.fn(); - data = { - _meta: { - hasPermissions: true, - }, - }; - jest.doMock('ui/notify', () => ({ - toastNotifications: { - addDanger, - }, - })); - setModulesAndMocks(true); - initSetupModeState(angularStateMock.scope, angularStateMock.injector); - await toggleSetupMode(true); - waitForSetupModeData(() => { - const state = getSetupModeState(); - expect(state.enabled).toBe(false); - expect(addDanger).toHaveBeenCalledWith({ - title: 'Setup mode is not available', - text: 'This feature is not available on cloud.', - }); - done(); - }); - }); - it('should not fetch data if the user does not have sufficient permissions', async done => { const addDanger = jest.fn(); - jest.doMock('ui/notify', () => ({ - toastNotifications: { - addDanger, + jest.doMock('../legacy_shims', () => ({ + Legacy: { + shims: { + toastNotifications: { + addDanger, + }, + }, }, })); data = { diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx similarity index 82% rename from x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx rename to x-pack/plugins/monitoring/public/lib/setup_mode.tsx index 7b081b79d6acd..5afb382b7cda8 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -7,19 +7,11 @@ import React from 'react'; import { render } from 'react-dom'; import { get, contains } from 'lodash'; -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; -import { PluginsSetup } from 'ui/new_platform/new_platform'; -import chrome from '../np_imports/ui/chrome'; -import { CloudSetup } from '../../../../../plugins/cloud/public'; +import { Legacy } from '../legacy_shims'; import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; -interface PluginsSetupWithCloud extends PluginsSetup { - cloud: CloudSetup; -} - function isOnPage(hash: string) { return contains(window.location.hash, hash); } @@ -46,12 +38,12 @@ const checkAngularState = () => { interface ISetupModeState { enabled: boolean; data: any; - callbacks: Function[]; + callback?: (() => void) | null; } const setupModeState: ISetupModeState = { enabled: false, data: null, - callbacks: [], + callback: null, }; export const getSetupModeState = () => setupModeState; @@ -93,18 +85,13 @@ export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid } }; -const notifySetupModeDataChange = (oldData?: any) => { - setupModeState.callbacks.forEach((cb: Function) => cb(oldData)); -}; +const notifySetupModeDataChange = () => setupModeState.callback && setupModeState.callback(); export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid = false) => { - const oldData = setupModeState.data; const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); setupModeState.data = data; - const { cloud } = npSetup.plugins as PluginsSetupWithCloud; - const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const hasPermissions = get(data, '_meta.hasPermissions', false); - if (isCloudEnabled || !hasPermissions) { + if (Legacy.shims.isCloud || !hasPermissions) { let text: string = ''; if (!hasPermissions) { text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { @@ -117,7 +104,7 @@ export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid } angularState.scope.$evalAsync(() => { - toastNotifications.addDanger({ + Legacy.shims.toastNotifications.addDanger({ title: i18n.translate('xpack.monitoring.setupMode.notAvailableTitle', { defaultMessage: 'Setup mode is not available', }), @@ -126,7 +113,7 @@ export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid }); return toggleSetupMode(false); // eslint-disable-line no-use-before-define } - notifySetupModeDataChange(oldData); + notifySetupModeDataChange(); const globalState = angularState.injector.get('globalState'); const clusterUuid = globalState.cluster_uuid; @@ -182,9 +169,7 @@ export const setSetupModeMenuItem = () => { } const globalState = angularState.injector.get('globalState'); - const { cloud } = npSetup.plugins as PluginsSetupWithCloud; - const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); - const enabled = !globalState.inSetupMode && !isCloudEnabled; + const enabled = !globalState.inSetupMode && !Legacy.shims.isCloud; render( , @@ -192,13 +177,13 @@ export const setSetupModeMenuItem = () => { ); }; -export const addSetupModeCallback = (callback: Function) => setupModeState.callbacks.push(callback); +export const addSetupModeCallback = (callback: () => void) => (setupModeState.callback = callback); -export const initSetupModeState = async ($scope: any, $injector: any, callback?: Function) => { +export const initSetupModeState = async ($scope: any, $injector: any, callback?: () => void) => { angularState.scope = $scope; angularState.injector = $injector; if (callback) { - setupModeState.callbacks.push(callback); + setupModeState.callback = callback; } const globalState = $injector.get('globalState'); @@ -212,7 +197,7 @@ export const isInSetupMode = () => { return true; } - const $injector = angularState.injector || chrome.dangerouslyGetActiveInjector(); + const $injector = angularState.injector || Legacy.shims.getAngularInjector(); const globalState = $injector.get('globalState'); return globalState.inSetupMode; }; diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts new file mode 100644 index 0000000000000..63f0c46c14096 --- /dev/null +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -0,0 +1,147 @@ +/* + * 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 { + App, + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from 'kibana/public'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; +import { MonitoringPluginDependencies, MonitoringConfig } from './types'; +import { + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + KIBANA_ALERTING_ENABLED, +} from '../common/constants'; + +export class MonitoringPlugin + implements Plugin { + constructor(private initializerContext: PluginInitializerContext) {} + + public setup( + core: CoreSetup, + plugins: object & { home?: HomePublicPluginSetup; cloud?: { isCloudEnabled: boolean } } + ) { + const { home } = plugins; + const id = 'monitoring'; + const icon = 'monitoringApp'; + const title = i18n.translate('xpack.monitoring.stackMonitoringTitle', { + defaultMessage: 'Stack Monitoring', + }); + const monitoring = this.initializerContext.config.get(); + + if (!monitoring.ui.enabled || !monitoring.enabled) { + return false; + } + + if (home) { + home.featureCatalogue.register({ + id, + title, + icon, + path: '/app/monitoring', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + description: i18n.translate('xpack.monitoring.monitoringDescription', { + defaultMessage: 'Track the real-time health and performance of your Elastic Stack.', + }), + }); + } + + const app: App = { + id, + title, + order: 9002, + euiIconType: icon, + category: DEFAULT_APP_CATEGORIES.management, + mount: async (params: AppMountParameters) => { + const [coreStart, pluginsStart] = await core.getStartServices(); + const { AngularApp } = await import('./angular'); + const deps: MonitoringPluginDependencies = { + navigation: pluginsStart.navigation, + element: params.element, + core: coreStart, + data: pluginsStart.data, + isCloud: Boolean(plugins.cloud?.isCloudEnabled), + pluginInitializerContext: this.initializerContext, + externalConfig: this.getExternalConfig(), + }; + + this.setInitialTimefilter(deps); + this.overrideAlertingEmailDefaults(deps); + + const monitoringApp = new AngularApp(deps); + const removeHistoryListener = params.history.listen(location => { + if (location.pathname === '' && location.hash === '') { + monitoringApp.applyScope(); + } + }); + + return () => { + removeHistoryListener(); + monitoringApp.destroy(); + }; + }, + }; + + core.application.register(app); + return true; + } + + public start(core: CoreStart, plugins: any) {} + + public stop() {} + + private setInitialTimefilter({ core: coreContext, data }: MonitoringPluginDependencies) { + const { timefilter } = data.query.timefilter; + const { uiSettings } = coreContext; + const refreshInterval = { value: 10000, pause: false }; + const time = { from: 'now-1h', to: 'now' }; + timefilter.setRefreshInterval(refreshInterval); + timefilter.setTime(time); + uiSettings.overrideLocalDefault( + 'timepicker:refreshIntervalDefaults', + JSON.stringify(refreshInterval) + ); + uiSettings.overrideLocalDefault('timepicker:timeDefaults', JSON.stringify(time)); + } + + private overrideAlertingEmailDefaults({ core: coreContext }: MonitoringPluginDependencies) { + const { uiSettings } = coreContext; + if (KIBANA_ALERTING_ENABLED && !uiSettings.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS)) { + uiSettings.overrideLocalDefault( + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + JSON.stringify({ + name: i18n.translate('xpack.monitoring.alertingEmailAddress.name', { + defaultMessage: 'Alerting email address', + }), + value: '', + description: i18n.translate('xpack.monitoring.alertingEmailAddress.description', { + defaultMessage: `The default email address to receive alerts from Stack Monitoring`, + }), + category: ['monitoring'], + }) + ); + } + } + + private getExternalConfig() { + const monitoring = this.initializerContext.config.get(); + return [ + ['minIntervalSeconds', monitoring.ui.min_interval_seconds], + ['showLicenseExpiration', monitoring.ui.show_license_expiration], + ['showCgroupMetricsElasticsearch', monitoring.ui.container.elasticsearch.enabled], + ['showCgroupMetricsLogstash', monitoring.ui.container.logstash.enabled], + ]; + } +} diff --git a/x-pack/legacy/plugins/monitoring/public/services/__tests__/breadcrumbs.js b/x-pack/plugins/monitoring/public/services/__tests__/breadcrumbs.js similarity index 96% rename from x-pack/legacy/plugins/monitoring/public/services/__tests__/breadcrumbs.js rename to x-pack/plugins/monitoring/public/services/__tests__/breadcrumbs.js index e5b2e01373340..d4493d4d39e58 100644 --- a/x-pack/legacy/plugins/monitoring/public/services/__tests__/breadcrumbs.js +++ b/x-pack/plugins/monitoring/public/services/__tests__/breadcrumbs.js @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { breadcrumbsProvider } from '../breadcrumbs_provider'; -import { MonitoringMainController } from 'plugins/monitoring/directives/main'; +import { breadcrumbsProvider } from '../breadcrumbs'; +import { MonitoringMainController } from '../../directives/main'; describe('Monitoring Breadcrumbs Service', () => { it('in Cluster Alerts', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/services/__tests__/executor_provider.js b/x-pack/plugins/monitoring/public/services/__tests__/executor.js similarity index 87% rename from x-pack/legacy/plugins/monitoring/public/services/__tests__/executor_provider.js rename to x-pack/plugins/monitoring/public/services/__tests__/executor.js index 2c4d49716406c..1113f9e32bdc7 100644 --- a/x-pack/legacy/plugins/monitoring/public/services/__tests__/executor_provider.js +++ b/x-pack/plugins/monitoring/public/services/__tests__/executor.js @@ -7,14 +7,22 @@ import ngMock from 'ng_mock'; import expect from '@kbn/expect'; import sinon from 'sinon'; -import { executorProvider } from '../executor_provider'; +import { executorProvider } from '../executor'; import Bluebird from 'bluebird'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { Legacy } from '../../legacy_shims'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; describe('$executor service', () => { let scope; let executor; let $timeout; + let timefilter; + + beforeEach(() => { + const data = dataPluginMock.createStartContract(); + Legacy._shims = { timefilter }; + timefilter = data.query.timefilter.timefilter; + }); beforeEach(ngMock.module('kibana')); diff --git a/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs_provider.js b/x-pack/plugins/monitoring/public/services/breadcrumbs.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/services/breadcrumbs_provider.js rename to x-pack/plugins/monitoring/public/services/breadcrumbs.js index 7917606a5bc8e..f2867180e9c4c 100644 --- a/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs_provider.js +++ b/x-pack/plugins/monitoring/public/services/breadcrumbs.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'plugins/monitoring/np_imports/ui/chrome'; +import { Legacy } from '../legacy_shims'; import { i18n } from '@kbn/i18n'; // Helper for making objects to use in a link element @@ -195,7 +195,7 @@ export function breadcrumbsProvider() { breadcrumbs = breadcrumbs.concat(getApmBreadcrumbs(mainInstance)); } - chrome.breadcrumbs.set( + Legacy.shims.breadcrumbs.set( breadcrumbs.map(b => ({ text: b.label, href: b.url, diff --git a/x-pack/legacy/plugins/monitoring/public/services/clusters.js b/x-pack/plugins/monitoring/public/services/clusters.js similarity index 77% rename from x-pack/legacy/plugins/monitoring/public/services/clusters.js rename to x-pack/plugins/monitoring/public/services/clusters.js index 40d6fa59228f8..dfa538b458054 100644 --- a/x-pack/legacy/plugins/monitoring/public/services/clusters.js +++ b/x-pack/plugins/monitoring/public/services/clusters.js @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { ajaxErrorHandlersProvider } from '../lib/ajax_error_handler'; +import { Legacy } from '../legacy_shims'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../common/constants'; function formatClusters(clusters) { @@ -20,10 +19,9 @@ function formatCluster(cluster) { return cluster; } -const uiModule = uiModules.get('monitoring/clusters'); -uiModule.service('monitoringClusters', $injector => { +export function monitoringClustersProvider($injector) { return (clusterUuid, ccs, codePaths) => { - const { min, max } = timefilter.getBounds(); + const { min, max } = Legacy.shims.timefilter.getBounds(); // append clusterUuid if the parameter is given let url = '../api/monitoring/v1/clusters'; @@ -51,4 +49,4 @@ uiModule.service('monitoringClusters', $injector => { return ajaxErrorHandlers(err); }); }; -}); +} diff --git a/x-pack/legacy/plugins/monitoring/public/services/executor_provider.js b/x-pack/plugins/monitoring/public/services/executor.js similarity index 66% rename from x-pack/legacy/plugins/monitoring/public/services/executor_provider.js rename to x-pack/plugins/monitoring/public/services/executor.js index 4a0551fa5af11..7c5e9d652b64e 100644 --- a/x-pack/legacy/plugins/monitoring/public/services/executor_provider.js +++ b/x-pack/plugins/monitoring/public/services/executor.js @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { subscribeWithScope } from 'plugins/monitoring/np_imports/ui/utils'; +import { Legacy } from '../legacy_shims'; +import { subscribeWithScope } from '../angular/helpers/utils'; import { Subscription } from 'rxjs'; -export function executorProvider(Promise, $timeout) { + +export function executorProvider($timeout, $q) { const queue = []; const subscriptions = new Subscription(); let executionTimer; @@ -61,15 +62,17 @@ export function executorProvider(Promise, $timeout) { * @returns {Promise} a promise of all the services */ function run() { - const noop = () => Promise.resolve(); - return Promise.all( - queue.map(service => { - return service - .execute() - .then(service.handleResponse || noop) - .catch(service.handleError || noop); - }) - ).finally(reset); + const noop = () => $q.resolve(); + return $q + .all( + queue.map(service => { + return service + .execute() + .then(service.handleResponse || noop) + .catch(service.handleError || noop); + }) + ) + .finally(reset); } function reFetch() { @@ -78,7 +81,7 @@ export function executorProvider(Promise, $timeout) { } function killIfPaused() { - if (timefilter.getRefreshInterval().pause) { + if (Legacy.shims.timefilter.getRefreshInterval().pause) { killTimer(); } } @@ -88,6 +91,7 @@ export function executorProvider(Promise, $timeout) { * @returns {void} */ function start() { + const timefilter = Legacy.shims.timefilter; if ( (ignorePaused || timefilter.getRefreshInterval().pause === false) && timefilter.getRefreshInterval().value > 0 @@ -102,17 +106,20 @@ export function executorProvider(Promise, $timeout) { return { register, start($scope) { - subscriptions.add( - subscribeWithScope($scope, timefilter.getFetch$(), { - next: reFetch, - }) - ); - subscriptions.add( - subscribeWithScope($scope, timefilter.getRefreshIntervalUpdate$(), { - next: killIfPaused, - }) - ); - start(); + $scope.$applyAsync(() => { + const timefilter = Legacy.shims.timefilter; + subscriptions.add( + subscribeWithScope($scope, timefilter.getFetch$(), { + next: reFetch, + }) + ); + subscriptions.add( + subscribeWithScope($scope, timefilter.getRefreshIntervalUpdate$(), { + next: killIfPaused, + }) + ); + start(); + }); }, run, destroy, diff --git a/x-pack/legacy/plugins/monitoring/public/services/features.js b/x-pack/plugins/monitoring/public/services/features.js similarity index 86% rename from x-pack/legacy/plugins/monitoring/public/services/features.js rename to x-pack/plugins/monitoring/public/services/features.js index e2357ef08d7df..f98af10f8dfb4 100644 --- a/x-pack/legacy/plugins/monitoring/public/services/features.js +++ b/x-pack/plugins/monitoring/public/services/features.js @@ -5,10 +5,8 @@ */ import _ from 'lodash'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -const uiModule = uiModules.get('monitoring/features', []); -uiModule.service('features', function($window) { +export function featuresProvider($window) { function getData() { let returnData = {}; const monitoringData = $window.localStorage.getItem('xpack.monitoring.data'); @@ -45,4 +43,4 @@ uiModule.service('features', function($window) { isEnabled, update, }; -}); +} diff --git a/x-pack/legacy/plugins/monitoring/public/services/license.js b/x-pack/plugins/monitoring/public/services/license.js similarity index 88% rename from x-pack/legacy/plugins/monitoring/public/services/license.js rename to x-pack/plugins/monitoring/public/services/license.js index 94078b799fdf1..341309004b110 100644 --- a/x-pack/legacy/plugins/monitoring/public/services/license.js +++ b/x-pack/plugins/monitoring/public/services/license.js @@ -5,11 +5,9 @@ */ import { contains } from 'lodash'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; import { ML_SUPPORTED_LICENSES } from '../../common/constants'; -const uiModule = uiModules.get('monitoring/license', []); -uiModule.service('license', () => { +export function licenseProvider() { return new (class LicenseService { constructor() { // do not initialize with usable state @@ -50,4 +48,4 @@ uiModule.service('license', () => { return false; } })(); -}); +} diff --git a/x-pack/legacy/plugins/monitoring/public/services/title.js b/x-pack/plugins/monitoring/public/services/title.js similarity index 54% rename from x-pack/legacy/plugins/monitoring/public/services/title.js rename to x-pack/plugins/monitoring/public/services/title.js index 442f4fb5b4029..0715f4dc9e0b6 100644 --- a/x-pack/legacy/plugins/monitoring/public/services/title.js +++ b/x-pack/plugins/monitoring/public/services/title.js @@ -6,21 +6,20 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { docTitle } from 'ui/doc_title'; +import { Legacy } from '../legacy_shims'; -const uiModule = uiModules.get('monitoring/title', []); -uiModule.service('title', () => { +export function titleProvider($rootScope) { return function changeTitle(cluster, suffix) { let clusterName = _.get(cluster, 'cluster_name'); clusterName = clusterName ? `- ${clusterName}` : ''; suffix = suffix ? `- ${suffix}` : ''; - docTitle.change( - i18n.translate('xpack.monitoring.stackMonitoringDocTitle', { - defaultMessage: 'Stack Monitoring {clusterName} {suffix}', - values: { clusterName, suffix }, - }), - true - ); + $rootScope.$applyAsync(() => { + Legacy.shims.docTitle.change( + i18n.translate('xpack.monitoring.stackMonitoringDocTitle', { + defaultMessage: 'Stack Monitoring {clusterName} {suffix}', + values: { clusterName, suffix }, + }) + ); + }); }; -}); +} diff --git a/x-pack/plugins/monitoring/public/types.ts b/x-pack/plugins/monitoring/public/types.ts new file mode 100644 index 0000000000000..5fcb6b50f5d83 --- /dev/null +++ b/x-pack/plugins/monitoring/public/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 { PluginInitializerContext, CoreStart } from 'kibana/public'; +import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; + +export { MonitoringConfig } from '../server'; + +export interface MonitoringPluginDependencies { + navigation: NavigationStart; + data: DataPublicPluginStart; + element: HTMLElement; + core: CoreStart; + isCloud: boolean; + pluginInitializerContext: PluginInitializerContext; + externalConfig: Array | Array>; +} diff --git a/x-pack/plugins/monitoring/public/url_state.ts b/x-pack/plugins/monitoring/public/url_state.ts new file mode 100644 index 0000000000000..e66d5462c2bb5 --- /dev/null +++ b/x-pack/plugins/monitoring/public/url_state.ts @@ -0,0 +1,169 @@ +/* + * 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 { Subscription } from 'rxjs'; +import { History } from 'history'; +import { createHashHistory } from 'history'; +import { MonitoringPluginDependencies } from './types'; + +import { + RefreshInterval, + TimeRange, + syncQueryStateWithUrl, +} from '../../../../src/plugins/data/public'; + +import { + createStateContainer, + createKbnUrlStateStorage, + StateContainer, + INullableBaseStateContainer, + IKbnUrlStateStorage, + ISyncStateRef, + syncState, +} from '../../../../src/plugins/kibana_utils/public'; + +interface Route { + params: { _g: unknown }; +} + +interface RawObject { + [key: string]: unknown; +} + +export interface MonitoringAppState { + [key: string]: unknown; + cluster_uuid?: string; + ccs?: boolean; + inSetupMode?: boolean; + refreshInterval?: RefreshInterval; + time?: TimeRange; + filters?: any[]; +} + +export interface MonitoringAppStateTransitions { + set: ( + state: MonitoringAppState + ) => ( + prop: T, + value: MonitoringAppState[T] + ) => MonitoringAppState; +} + +const GLOBAL_STATE_KEY = '_g'; +const objectEquals = (objA: any, objB: any) => JSON.stringify(objA) === JSON.stringify(objB); + +export class GlobalState { + private readonly stateSyncRef: ISyncStateRef; + private readonly stateContainer: StateContainer< + MonitoringAppState, + MonitoringAppStateTransitions + >; + private readonly stateStorage: IKbnUrlStateStorage; + private readonly stateContainerChangeSub: Subscription; + private readonly syncQueryStateWithUrlManager: { stop: () => void }; + private readonly timefilterRef: MonitoringPluginDependencies['data']['query']['timefilter']['timefilter']; + + private lastAssignedState: MonitoringAppState = {}; + private lastKnownGlobalState?: string; + + constructor( + queryService: MonitoringPluginDependencies['data']['query'], + rootScope: ng.IRootScopeService, + ngLocation: ng.ILocationService, + externalState: RawObject + ) { + this.timefilterRef = queryService.timefilter.timefilter; + + const history: History = createHashHistory(); + this.stateStorage = createKbnUrlStateStorage({ useHash: false, history }); + + const initialStateFromUrl = this.stateStorage.get(GLOBAL_STATE_KEY) as MonitoringAppState; + + this.stateContainer = createStateContainer(initialStateFromUrl, { + set: state => (prop, value) => ({ ...state, [prop]: value }), + }); + + this.stateSyncRef = syncState({ + storageKey: GLOBAL_STATE_KEY, + stateContainer: this.stateContainer as INullableBaseStateContainer, + stateStorage: this.stateStorage, + }); + + this.stateContainerChangeSub = this.stateContainer.state$.subscribe(() => { + this.lastAssignedState = this.getState(); + if (!this.stateContainer.get() && this.lastKnownGlobalState) { + rootScope.$applyAsync(() => + ngLocation.search(`${GLOBAL_STATE_KEY}=${this.lastKnownGlobalState}`).replace() + ); + } + this.syncExternalState(externalState); + }); + + this.syncQueryStateWithUrlManager = syncQueryStateWithUrl(queryService, this.stateStorage); + this.stateSyncRef.start(); + this.startHashSync(rootScope, ngLocation); + this.lastAssignedState = this.getState(); + + rootScope.$on('$destroy', () => this.destroy()); + } + + private syncExternalState(externalState: { [key: string]: unknown }) { + const currentState = this.stateContainer.get(); + for (const key in currentState) { + if ( + ({ save: 1, time: 1, refreshInterval: 1, filters: 1 } as { [key: string]: number })[key] + ) { + continue; + } + if (currentState[key] !== externalState[key]) { + externalState[key] = currentState[key]; + } + } + } + + private startHashSync(rootScope: ng.IRootScopeService, ngLocation: ng.ILocationService) { + rootScope.$on( + '$routeChangeStart', + (_: { preventDefault: () => void }, newState: Route, oldState: Route) => { + const currentGlobalState = oldState?.params?._g; + const nextGlobalState = newState?.params?._g; + if (!nextGlobalState && currentGlobalState && typeof currentGlobalState === 'string') { + newState.params._g = currentGlobalState; + ngLocation.search(`${GLOBAL_STATE_KEY}=${currentGlobalState}`).replace(); + } + this.lastKnownGlobalState = (nextGlobalState || currentGlobalState) as string; + } + ); + } + + public setState(state?: { [key: string]: unknown }) { + const currentAppState = this.getState(); + const newAppState = { ...currentAppState, ...state }; + if (state && objectEquals(newAppState, currentAppState)) { + return; + } + const newState = { + ...newAppState, + refreshInterval: this.timefilterRef.getRefreshInterval(), + time: this.timefilterRef.getTime(), + }; + this.lastAssignedState = newState; + this.stateContainer.set(newState); + } + + public getState(): MonitoringAppState { + const currentState = { ...this.lastAssignedState, ...this.stateContainer.get() }; + delete currentState.filters; + const { refreshInterval: _nullA, time: _nullB, ...currentAppState } = currentState; + return currentAppState || {}; + } + + public destroy() { + this.syncQueryStateWithUrlManager.stop(); + this.stateContainerChangeSub.unsubscribe(); + this.stateSyncRef.stop(); + } +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/__tests__/base_controller.js b/x-pack/plugins/monitoring/public/views/__tests__/base_controller.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/views/__tests__/base_controller.js rename to x-pack/plugins/monitoring/public/views/__tests__/base_controller.js index 6c3c73a35601c..4f7180b138aa5 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/__tests__/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/__tests__/base_controller.js @@ -7,8 +7,9 @@ import { spy, stub } from 'sinon'; import expect from '@kbn/expect'; import { MonitoringViewBaseController } from '../'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { Legacy } from '../../legacy_shims'; import { PromiseWithCancel, Status } from '../../../common/cancel_promise'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; /* * Mostly copied from base_table_controller test, with modifications @@ -21,9 +22,13 @@ describe('MonitoringViewBaseController', function() { let titleService; let executorService; let configService; + let timefilter; const httpCall = ms => new Promise(resolve => setTimeout(() => resolve(), ms)); before(() => { + const data = dataPluginMock.createStartContract(); + Legacy._shims = { timefilter }; + timefilter = data.query.timefilter.timefilter; titleService = spy(); executorService = { register: spy(), diff --git a/x-pack/legacy/plugins/monitoring/public/views/__tests__/base_table_controller.js b/x-pack/plugins/monitoring/public/views/__tests__/base_table_controller.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/__tests__/base_table_controller.js rename to x-pack/plugins/monitoring/public/views/__tests__/base_table_controller.js diff --git a/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html b/x-pack/plugins/monitoring/public/views/access_denied/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html rename to x-pack/plugins/monitoring/public/views/access_denied/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.js b/x-pack/plugins/monitoring/public/views/access_denied/index.js similarity index 89% rename from x-pack/legacy/plugins/monitoring/public/views/access_denied/index.js rename to x-pack/plugins/monitoring/public/views/access_denied/index.js index a0cfc79f001ca..856e59702963a 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.js +++ b/x-pack/plugins/monitoring/public/views/access_denied/index.js @@ -5,8 +5,8 @@ */ import { noop } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import uiChrome from 'plugins/monitoring/np_imports/ui/chrome'; +import { uiRoutes } from '../../angular/helpers/routes'; +import { Legacy } from '../../legacy_shims'; import template from './index.html'; const tryPrivilege = ($http, kbnUrl) => { @@ -40,7 +40,7 @@ uiRoutes.when('/access-denied', { // The template's "Back to Kibana" button click handler this.goToKibana = () => { - $window.location.href = uiChrome.getBasePath() + kbnBaseUrl; + $window.location.href = Legacy.shims.getBasePath() + kbnBaseUrl; }; // keep trying to load data in the background diff --git a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.html b/x-pack/plugins/monitoring/public/views/alerts/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/alerts/index.html rename to x-pack/plugins/monitoring/public/views/alerts/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js b/x-pack/plugins/monitoring/public/views/alerts/index.js similarity index 77% rename from x-pack/legacy/plugins/monitoring/public/views/alerts/index.js rename to x-pack/plugins/monitoring/public/views/alerts/index.js index 62cc985887e9f..2e7a34c0aef29 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js +++ b/x-pack/plugins/monitoring/public/views/alerts/index.js @@ -8,12 +8,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { render } from 'react-dom'; import { find, get } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; +import { uiRoutes } from '../../angular/helpers/routes'; import template from './index.html'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { I18nContext } from 'ui/i18n'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { routeInitProvider } from '../../lib/route_init'; +import { ajaxErrorHandlersProvider } from '../../lib/ajax_error_handler'; +import { Legacy } from '../../legacy_shims'; import { Alerts } from '../../components/alerts'; import { MonitoringViewBaseEuiTableController } from '../base_eui_table_controller'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -28,7 +27,7 @@ function getPageData($injector) { ? `../api/monitoring/v1/alert_status` : `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`; - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); const data = { timeRange: { min: timeBounds.min.toISOString(), @@ -103,22 +102,20 @@ uiRoutes.when('/alerts', { ); render( - - - - - {app} - - - - - - - - , + + + + {app} + + + + + + + , document.getElementById('monitoringAlertsApp') ); }; diff --git a/x-pack/legacy/plugins/monitoring/public/views/all.js b/x-pack/plugins/monitoring/public/views/all.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/views/all.js rename to x-pack/plugins/monitoring/public/views/all.js index ded378b050c2d..51dcce751863c 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/all.js +++ b/x-pack/plugins/monitoring/public/views/all.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './loading'; import './no_data'; import './access_denied'; import './alerts'; diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/instance/index.html b/x-pack/plugins/monitoring/public/views/apm/instance/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/apm/instance/index.html rename to x-pack/plugins/monitoring/public/views/apm/instance/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/instance/index.js b/x-pack/plugins/monitoring/public/views/apm/instance/index.js similarity index 83% rename from x-pack/legacy/plugins/monitoring/public/views/apm/instance/index.js rename to x-pack/plugins/monitoring/public/views/apm/instance/index.js index 4d0f858d28117..982857ab5aea4 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/apm/instance/index.js +++ b/x-pack/plugins/monitoring/public/views/apm/instance/index.js @@ -13,12 +13,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { find, get } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import template from './index.html'; import { MonitoringViewBaseController } from '../../base_controller'; import { ApmServerInstance } from '../../../components/apm/instance'; -import { I18nContext } from 'ui/i18n'; import { CODE_PATH_APM } from '../../../../common/constants'; uiRoutes.when('/apm/instances/:uuid', { @@ -64,14 +63,12 @@ uiRoutes.when('/apm/instances/:uuid', { renderReact(data) { const component = ( - - - + ); super.renderReact(component); } diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.html b/x-pack/plugins/monitoring/public/views/apm/instances/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.html rename to x-pack/plugins/monitoring/public/views/apm/instances/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.js b/x-pack/plugins/monitoring/public/views/apm/instances/index.js similarity index 70% rename from x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.js rename to x-pack/plugins/monitoring/public/views/apm/instances/index.js index 317879063b6e5..8cd0b03e89e04 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.js +++ b/x-pack/plugins/monitoring/public/views/apm/instances/index.js @@ -7,12 +7,11 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import template from './index.html'; import { ApmServerInstances } from '../../../components/apm/instances'; import { MonitoringViewBaseEuiTableController } from '../..'; -import { I18nContext } from 'ui/i18n'; import { SetupModeRenderer } from '../../../components/renderers'; import { APM_SYSTEM_ID, CODE_PATH_APM } from '../../../../common/constants'; @@ -62,28 +61,26 @@ uiRoutes.when('/apm/instances', { const { pagination, sorting, onTableChange } = this; const component = ( - - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> ); super.renderReact(component); } diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/overview/index.html b/x-pack/plugins/monitoring/public/views/apm/overview/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/apm/overview/index.html rename to x-pack/plugins/monitoring/public/views/apm/overview/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/overview/index.js b/x-pack/plugins/monitoring/public/views/apm/overview/index.js similarity index 81% rename from x-pack/legacy/plugins/monitoring/public/views/apm/overview/index.js rename to x-pack/plugins/monitoring/public/views/apm/overview/index.js index e6562f428d2a0..1fdd441e62e22 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/apm/overview/index.js +++ b/x-pack/plugins/monitoring/public/views/apm/overview/index.js @@ -6,12 +6,11 @@ import React from 'react'; import { find } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import template from './index.html'; import { MonitoringViewBaseController } from '../../base_controller'; import { ApmOverview } from '../../../components/apm/overview'; -import { I18nContext } from 'ui/i18n'; import { CODE_PATH_APM } from '../../../../common/constants'; uiRoutes.when('/apm', { @@ -48,11 +47,7 @@ uiRoutes.when('/apm', { } renderReact(data) { - const component = ( - - - - ); + const component = ; super.renderReact(component); } }, diff --git a/x-pack/legacy/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js similarity index 77% rename from x-pack/legacy/plugins/monitoring/public/views/base_controller.js rename to x-pack/plugins/monitoring/public/views/base_controller.js index 25b4d97177a98..e5e59f2f8e826 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_controller.js @@ -8,9 +8,8 @@ import React from 'react'; import moment from 'moment'; import { render, unmountComponentAtNode } from 'react-dom'; import { getPageData } from '../lib/get_page_data'; -import { PageLoading } from 'plugins/monitoring/components'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { I18nContext } from 'ui/i18n'; +import { PageLoading } from '../components'; +import { Legacy } from '../legacy_shims'; import { PromiseWithCancel } from '../../common/cancel_promise'; import { updateSetupModeData, getSetupModeState } from '../lib/setup_mode'; @@ -113,18 +112,6 @@ export class MonitoringViewBaseController { const { enableTimeFilter = true, enableAutoRefresh = true } = options; - if (enableTimeFilter === false) { - timefilter.disableTimeRangeSelector(); - } else { - timefilter.enableTimeRangeSelector(); - } - - if (enableAutoRefresh === false) { - timefilter.disableAutoRefreshSelector(); - } else { - timefilter.enableAutoRefreshSelector(); - } - this.updateData = () => { if (this.updateDataPromise) { // Do not sent another request if one is inflight @@ -146,7 +133,46 @@ export class MonitoringViewBaseController { }); }); }; - fetchDataImmediately && this.updateData(); + + $scope.$applyAsync(() => { + const timefilter = Legacy.shims.timefilter; + + if (enableTimeFilter === false) { + timefilter.disableTimeRangeSelector(); + } else { + timefilter.enableTimeRangeSelector(); + } + + if (enableAutoRefresh === false) { + timefilter.disableAutoRefreshSelector(); + } else { + timefilter.enableAutoRefreshSelector(); + } + + // needed for chart pages + this.onBrush = ({ xaxis }) => { + removePopstateHandler(); + const { to, from } = xaxis; + const timezone = config.get('dateFormat:tz'); + const offset = getOffsetInMS(timezone); + timefilter.setTime({ + from: moment(from - offset), + to: moment(to - offset), + mode: 'absolute', + }); + $executor.cancel(); + $executor.run(); + ++zoomInLevel; + clearTimeout(deferTimer); + /* + Needed to defer 'popstate' event, so it does not fire immediately after it's added. + 10ms is to make sure the event is not added with the same code digest + */ + deferTimer = setTimeout(() => addPopstateHandler(), 10); + }; + + fetchDataImmediately && this.updateData(); + }); $executor.register({ execute: () => this.updateData(), @@ -155,35 +181,14 @@ export class MonitoringViewBaseController { $scope.$on('$destroy', () => { clearTimeout(deferTimer); removePopstateHandler(); - if (this.reactNodeId) { + const targetElement = document.getElementById(this.reactNodeId); + if (targetElement) { // WIP https://github.com/elastic/x-pack-kibana/issues/5198 - unmountComponentAtNode(document.getElementById(this.reactNodeId)); + unmountComponentAtNode(targetElement); } $executor.destroy(); }); - // needed for chart pages - this.onBrush = ({ xaxis }) => { - removePopstateHandler(); - const { to, from } = xaxis; - const timezone = config.get('dateFormat:tz'); - const offset = getOffsetInMS(timezone); - timefilter.setTime({ - from: moment(from - offset), - to: moment(to - offset), - mode: 'absolute', - }); - $executor.cancel(); - $executor.run(); - ++zoomInLevel; - clearTimeout(deferTimer); - /* - Needed to defer 'popstate' event, so it does not fire immediately after it's added. - 10ms is to make sure the event is not added with the same code digest - */ - deferTimer = setTimeout(() => addPopstateHandler(), 10); - }; - this.setTitle = title => titleService($scope.cluster, title); } @@ -193,16 +198,11 @@ export class MonitoringViewBaseController { console.warn(`"#${this.reactNodeId}" element has not been added to the DOM yet`); return; } - if (this._isDataInitialized === false) { - render( - - - , - renderElement - ); - } else { - render(component, renderElement); - } + const I18nContext = Legacy.shims.I18nContext; + const wrappedComponent = ( + {!this._isDataInitialized ? : component} + ); + render(wrappedComponent, renderElement); } getPaginationRouteOptions() { diff --git a/x-pack/legacy/plugins/monitoring/public/views/base_eui_table_controller.js b/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/views/base_eui_table_controller.js rename to x-pack/plugins/monitoring/public/views/base_eui_table_controller.js index 0460adaeecd10..ac5ba45af8614 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/base_eui_table_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js @@ -5,7 +5,7 @@ */ import { MonitoringViewBaseController } from './'; -import { euiTableStorageGetter, euiTableStorageSetter } from 'plugins/monitoring/components/table'; +import { euiTableStorageGetter, euiTableStorageSetter } from '../components/table'; import { EUI_SORT_ASCENDING } from '../../common/constants'; const PAGE_SIZE_OPTIONS = [5, 10, 20, 50]; diff --git a/x-pack/legacy/plugins/monitoring/public/views/base_table_controller.js b/x-pack/plugins/monitoring/public/views/base_table_controller.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/views/base_table_controller.js rename to x-pack/plugins/monitoring/public/views/base_table_controller.js index 6ae486eae96fc..2275608c473dd 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/base_table_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_table_controller.js @@ -5,7 +5,7 @@ */ import { MonitoringViewBaseController } from './'; -import { tableStorageGetter, tableStorageSetter } from 'plugins/monitoring/components/table'; +import { tableStorageGetter, tableStorageSetter } from '../components/table'; /** * Class to manage common instantiation behaviors in a view controller diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/beat/get_page_data.js b/x-pack/plugins/monitoring/public/views/beats/beat/get_page_data.js similarity index 82% rename from x-pack/legacy/plugins/monitoring/public/views/beats/beat/get_page_data.js rename to x-pack/plugins/monitoring/public/views/beats/beat/get_page_data.js index 7e77e93d52fe8..7c9c459218529 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/beat/get_page_data.js +++ b/x-pack/plugins/monitoring/public/views/beats/beat/get_page_data.js @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; export function getPageData($injector) { const $http = $injector.get('$http'); const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/beats/beat/${$route.current.params.beatUuid}`; - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/beat/index.html b/x-pack/plugins/monitoring/public/views/beats/beat/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/beats/beat/index.html rename to x-pack/plugins/monitoring/public/views/beats/beat/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/beat/index.js b/x-pack/plugins/monitoring/public/views/beats/beat/index.js similarity index 92% rename from x-pack/legacy/plugins/monitoring/public/views/beats/beat/index.js rename to x-pack/plugins/monitoring/public/views/beats/beat/index.js index b3fad1b4cc3cb..9d5f9b4d562a6 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/beat/index.js +++ b/x-pack/plugins/monitoring/public/views/beats/beat/index.js @@ -6,8 +6,8 @@ import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import { MonitoringViewBaseController } from '../../'; import { getPageData } from './get_page_data'; import template from './index.html'; diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/listing/get_page_data.js b/x-pack/plugins/monitoring/public/views/beats/listing/get_page_data.js similarity index 80% rename from x-pack/legacy/plugins/monitoring/public/views/beats/listing/get_page_data.js rename to x-pack/plugins/monitoring/public/views/beats/listing/get_page_data.js index 1838011dee652..77942303afcc2 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/listing/get_page_data.js +++ b/x-pack/plugins/monitoring/public/views/beats/listing/get_page_data.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; export function getPageData($injector) { const $http = $injector.get('$http'); const globalState = $injector.get('globalState'); - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/beats/beats`; return $http diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.html b/x-pack/plugins/monitoring/public/views/beats/listing/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.html rename to x-pack/plugins/monitoring/public/views/beats/listing/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.js b/x-pack/plugins/monitoring/public/views/beats/listing/index.js similarity index 66% rename from x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.js rename to x-pack/plugins/monitoring/public/views/beats/listing/index.js index 48848007c9c27..7d011089fdd7d 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.js +++ b/x-pack/plugins/monitoring/public/views/beats/listing/index.js @@ -6,13 +6,12 @@ import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import { MonitoringViewBaseEuiTableController } from '../../'; import { getPageData } from './get_page_data'; import template from './index.html'; import React, { Fragment } from 'react'; -import { I18nContext } from 'ui/i18n'; import { Listing } from '../../../components/beats/listing/listing'; import { SetupModeRenderer } from '../../../components/renderers'; import { CODE_PATH_BEATS, BEATS_SYSTEM_ID } from '../../../../common/constants'; @@ -62,31 +61,29 @@ uiRoutes.when('/beats/beats', { renderComponent() { const { sorting, pagination, onTableChange } = this.scope.beats; this.renderReact( - - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> ); } }, diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/overview/get_page_data.js b/x-pack/plugins/monitoring/public/views/beats/overview/get_page_data.js similarity index 80% rename from x-pack/legacy/plugins/monitoring/public/views/beats/overview/get_page_data.js rename to x-pack/plugins/monitoring/public/views/beats/overview/get_page_data.js index a3b120b277b94..e5d576961b797 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/overview/get_page_data.js +++ b/x-pack/plugins/monitoring/public/views/beats/overview/get_page_data.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; export function getPageData($injector) { const $http = $injector.get('$http'); const globalState = $injector.get('globalState'); - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/beats`; return $http diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/overview/index.html b/x-pack/plugins/monitoring/public/views/beats/overview/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/beats/overview/index.html rename to x-pack/plugins/monitoring/public/views/beats/overview/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/overview/index.js b/x-pack/plugins/monitoring/public/views/beats/overview/index.js similarity index 91% rename from x-pack/legacy/plugins/monitoring/public/views/beats/overview/index.js rename to x-pack/plugins/monitoring/public/views/beats/overview/index.js index aea62d5c7f78f..0eb39ef372263 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/overview/index.js +++ b/x-pack/plugins/monitoring/public/views/beats/overview/index.js @@ -6,8 +6,8 @@ import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import { MonitoringViewBaseController } from '../../'; import { getPageData } from './get_page_data'; import template from './index.html'; diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/listing/index.html b/x-pack/plugins/monitoring/public/views/cluster/listing/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/cluster/listing/index.html rename to x-pack/plugins/monitoring/public/views/cluster/listing/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/listing/index.js b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js similarity index 75% rename from x-pack/legacy/plugins/monitoring/public/views/cluster/listing/index.js rename to x-pack/plugins/monitoring/public/views/cluster/listing/index.js index 958226514b146..42be4f02f5c94 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/cluster/listing/index.js +++ b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js @@ -5,10 +5,9 @@ */ import React from 'react'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import { MonitoringViewBaseEuiTableController } from '../../'; -import { I18nContext } from 'ui/i18n'; import template from './index.html'; import { Listing } from '../../../components/cluster/listing'; import { CODE_PATH_ALL } from '../../../../common/constants'; @@ -33,7 +32,7 @@ uiRoutes } if (clusters.length === 1) { // Bypass the cluster listing if there is just 1 cluster - kbnUrl.changePath('/overview'); + kbnUrl.redirect('/overview'); return Promise.reject(); } return clusters; @@ -63,21 +62,19 @@ uiRoutes () => this.data, data => { this.renderReact( - - - + ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.html b/x-pack/plugins/monitoring/public/views/cluster/overview/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.html rename to x-pack/plugins/monitoring/public/views/cluster/overview/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js similarity index 68% rename from x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js rename to x-pack/plugins/monitoring/public/views/cluster/overview/index.js index e1777b8ed7b49..3f6fb77f02288 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js +++ b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js @@ -5,14 +5,13 @@ */ import React, { Fragment } from 'react'; import { isEmpty } from 'lodash'; -import chrome from 'ui/chrome'; +import { Legacy } from '../../../legacy_shims'; import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import template from './index.html'; import { MonitoringViewBaseController } from '../../'; -import { Overview } from 'plugins/monitoring/components/cluster/overview'; -import { I18nContext } from 'ui/i18n'; +import { Overview } from '../../../components/cluster/overview'; import { SetupModeRenderer } from '../../../components/renderers'; import { CODE_PATH_ALL, @@ -70,31 +69,29 @@ uiRoutes.when('/overview', { return; } - let emailAddress = chrome.getInjected('monitoringLegacyEmailAddress') || ''; + let emailAddress = Legacy.shims.getInjected('monitoringLegacyEmailAddress') || ''; if (KIBANA_ALERTING_ENABLED) { emailAddress = config.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS) || emailAddress; } this.renderReact( - - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js similarity index 80% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js index 83dd24209dfe3..d4a86b00a7505 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; export function getPageData($injector) { const $http = $injector.get('$http'); const globalState = $injector.get('globalState'); - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ccr`; return $http diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js similarity index 83% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/index.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js index cf51347842f4a..88d3a3614243f 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js @@ -6,13 +6,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; +import { uiRoutes } from '../../../angular/helpers/routes'; import { getPageData } from './get_page_data'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { routeInitProvider } from '../../../lib/route_init'; import template from './index.html'; import { Ccr } from '../../../components/elasticsearch/ccr'; import { MonitoringViewBaseController } from '../../base_controller'; -import { I18nContext } from 'ui/i18n'; import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; uiRoutes.when('/elasticsearch/ccr', { @@ -45,11 +44,7 @@ uiRoutes.when('/elasticsearch/ccr', { ); this.renderReact = ({ data }) => { - super.renderReact( - - - - ); + super.renderReact(); }; } }, diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js similarity index 82% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js index 22ca094d28b07..20f39edbb6a75 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; +import { Legacy } from '../../../../legacy_shims'; export function getPageData($injector) { const $http = $injector.get('$http'); const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ccr/${$route.current.params.index}/shard/${$route.current.params.shardId}`; return $http diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js similarity index 86% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js index ff35f7f743f66..260422d322a2d 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js @@ -7,13 +7,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; +import { uiRoutes } from '../../../../angular/helpers/routes'; import { getPageData } from './get_page_data'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { routeInitProvider } from '../../../../lib/route_init'; import template from './index.html'; import { MonitoringViewBaseController } from '../../../base_controller'; import { CcrShard } from '../../../../components/elasticsearch/ccr_shard'; -import { I18nContext } from 'ui/i18n'; import { CODE_PATH_ELASTICSEARCH } from '../../../../../common/constants'; uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { @@ -54,11 +53,7 @@ uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { ); this.renderReact = props => { - super.renderReact( - - - - ); + super.renderReact(); }; } }, diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/advanced/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/advanced/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js similarity index 79% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js index 4fc439b4e0123..9fbaf6c00725d 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js @@ -9,13 +9,12 @@ */ import React from 'react'; import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../../lib/route_init'; import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { Legacy } from '../../../../legacy_shims'; import { AdvancedIndex } from '../../../../components/elasticsearch/index/advanced'; -import { I18nContext } from 'ui/i18n'; import { MonitoringViewBaseController } from '../../../base_controller'; import { CODE_PATH_ELASTICSEARCH } from '../../../../../common/constants'; @@ -24,7 +23,7 @@ function getPageData($injector) { const $route = $injector.get('$route'); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/indices/${$route.current.params.index}`; const $http = $injector.get('$http'); - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { @@ -78,14 +77,12 @@ uiRoutes.when('/elasticsearch/indices/:index/advanced', { () => this.data, data => { this.renderReact( - - - + ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/index/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js similarity index 80% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/index.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js index bbeef8294a897..d2f49d2280e15 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js @@ -9,12 +9,11 @@ */ import React from 'react'; import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { I18nContext } from 'ui/i18n'; +import { Legacy } from '../../../legacy_shims'; import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; import { indicesByNodes } from '../../../components/elasticsearch/shard_allocation/transformers/indices_by_nodes'; import { Index } from '../../../components/elasticsearch/index/index'; @@ -26,7 +25,7 @@ function getPageData($injector) { const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/indices/${$route.current.params.index}`; - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { @@ -96,17 +95,15 @@ uiRoutes.when('/elasticsearch/indices/:index', { } this.renderReact( - - - + ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/indices/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/indices/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js similarity index 80% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/indices/index.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js index f1d96557b0c1c..ee177c3e8ed04 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/indices/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js @@ -7,12 +7,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import { MonitoringViewBaseEuiTableController } from '../../'; import { ElasticsearchIndices } from '../../../components'; import template from './index.html'; -import { I18nContext } from 'ui/i18n'; import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; uiRoutes.when('/elasticsearch/indices', { @@ -71,17 +70,15 @@ uiRoutes.when('/elasticsearch/indices', { this.renderReact = ({ clusterStatus, indices }) => { super.renderReact( - - - + ); }; } diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js similarity index 80% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js index 1943b580f7a75..0b50a04d53036 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; export function getPageData($injector) { const $http = $injector.get('$http'); const globalState = $injector.get('globalState'); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ml_jobs`; - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { ccs: globalState.ccs, diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js similarity index 92% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js index 5e66a4147ab70..d1dd81223ad5e 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js @@ -6,8 +6,8 @@ import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import { MonitoringViewBaseEuiTableController } from '../../'; import { getPageData } from './get_page_data'; import template from './index.html'; diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/advanced/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/advanced/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js similarity index 78% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js index 2bbdf604d00ce..5c45509fce37c 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js @@ -9,12 +9,11 @@ */ import React from 'react'; import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../../lib/route_init'; import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { I18nContext } from 'ui/i18n'; +import { Legacy } from '../../../../legacy_shims'; import { AdvancedNode } from '../../../../components/elasticsearch/node/advanced'; import { MonitoringViewBaseController } from '../../../base_controller'; import { CODE_PATH_ELASTICSEARCH } from '../../../../../common/constants'; @@ -23,7 +22,7 @@ function getPageData($injector) { const $http = $injector.get('$http'); const globalState = $injector.get('globalState'); const $route = $injector.get('$route'); - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/nodes/${$route.current.params.node}`; return $http @@ -79,14 +78,12 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', { ); this.renderReact( - - - + ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js similarity index 84% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js index 0d9e0b25eacd0..6aaa8aa452f68 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; export function getPageData($injector) { const $http = $injector.get('$http'); @@ -14,7 +14,7 @@ export function getPageData($injector) { const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/nodes/${$route.current.params.node}`; const features = $injector.get('features'); const showSystemIndices = features.isEnabled('showSystemIndices', false); - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/node/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js similarity index 84% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/index.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js index fa76222d78e2d..c34be490d1711 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js @@ -10,12 +10,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { partial } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import { getPageData } from './get_page_data'; import template from './index.html'; import { Node } from '../../../components/elasticsearch/node/node'; -import { I18nContext } from 'ui/i18n'; import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; import { nodesByIndices } from '../../../components/elasticsearch/shard_allocation/transformers/nodes_by_indices'; import { MonitoringViewBaseController } from '../../base_controller'; @@ -79,17 +78,15 @@ uiRoutes.when('/elasticsearch/nodes/:node', { $scope.labels = labels.node; this.renderReact( - - - + ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js similarity index 74% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js index a9a6774d4c883..db4aaca15c00e 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js @@ -7,13 +7,12 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { Legacy } from '../../../legacy_shims'; import template from './index.html'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { routeInitProvider } from '../../../lib/route_init'; import { MonitoringViewBaseEuiTableController } from '../../'; import { ElasticsearchNodes } from '../../../components'; -import { I18nContext } from 'ui/i18n'; import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; import { SetupModeRenderer } from '../../../components/renderers'; import { ELASTICSEARCH_SYSTEM_ID, CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; @@ -42,7 +41,7 @@ uiRoutes.when('/elasticsearch/nodes', { _api; // to fix eslint const $http = $injector.get('$http'); const globalState = $injector.get('globalState'); - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); const getNodes = (clusterUuid = globalState.cluster_uuid) => $http.post(`../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes`, { @@ -91,27 +90,25 @@ uiRoutes.when('/elasticsearch/nodes', { }; super.renderReact( - - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> ); }; } diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/controller.js b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js similarity index 83% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/controller.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js index 9f59b4d632222..7cdd7dfae0af0 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/controller.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js @@ -7,8 +7,7 @@ import React from 'react'; import { find } from 'lodash'; import { MonitoringViewBaseController } from '../../'; -import { ElasticsearchOverview } from 'plugins/monitoring/components'; -import { I18nContext } from 'ui/i18n'; +import { ElasticsearchOverview } from '../../../components'; export class ElasticsearchOverviewController extends MonitoringViewBaseController { constructor($injector, $scope) { @@ -78,19 +77,17 @@ export class ElasticsearchOverviewController extends MonitoringViewBaseControlle const { clusterStatus, metrics, shardActivity, logs } = data; const shardActivityData = shardActivity && this.filterShardActivityData(shardActivity); // no filter on data = null const component = ( - - - + ); super.renderReact(component); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.js similarity index 84% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/index.js rename to x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.js index 475c0fc494857..1c27a4d004abe 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import template from './index.html'; import { ElasticsearchOverviewController } from './controller'; import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; diff --git a/x-pack/legacy/plugins/monitoring/public/views/index.js b/x-pack/plugins/monitoring/public/views/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/index.js rename to x-pack/plugins/monitoring/public/views/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/instance/index.html b/x-pack/plugins/monitoring/public/views/kibana/instance/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/kibana/instance/index.html rename to x-pack/plugins/monitoring/public/views/kibana/instance/index.html diff --git a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js new file mode 100644 index 0000000000000..b743b4e49f096 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js @@ -0,0 +1,150 @@ +/* + * 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. + */ + +/* + * Kibana Instance + */ +import React from 'react'; +import { get } from 'lodash'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../lib/route_init'; +import template from './index.html'; +import { Legacy } from '../../../legacy_shims'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiFlexGrid, + EuiFlexItem, + EuiPanel, +} from '@elastic/eui'; +import { MonitoringTimeseriesContainer } from '../../../components/chart'; +import { DetailStatus } from '../../../components/kibana/detail_status'; +import { MonitoringViewBaseController } from '../../base_controller'; +import { CODE_PATH_KIBANA } from '../../../../common/constants'; + +function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const $route = $injector.get('$route'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana/${$route.current.params.uuid}`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} + +uiRoutes.when('/kibana/instances/:uuid', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_KIBANA] }); + }, + pageData: getPageData, + }, + controllerAs: 'monitoringKibanaInstanceApp', + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + super({ + title: `Kibana - ${get($scope.pageData, 'kibanaSummary.name')}`, + defaultData: {}, + getPageData, + reactNodeId: 'monitoringKibanaInstanceApp', + $scope, + $injector, + }); + + $scope.$watch( + () => this.data, + data => { + if (!data || !data.metrics) { + return; + } + + this.setTitle(`Kibana - ${get(data, 'kibanaSummary.name')}`); + + this.renderReact( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } + ); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/get_page_data.js b/x-pack/plugins/monitoring/public/views/kibana/instances/get_page_data.js similarity index 80% rename from x-pack/legacy/plugins/monitoring/public/views/kibana/instances/get_page_data.js rename to x-pack/plugins/monitoring/public/views/kibana/instances/get_page_data.js index 4f8d7fa20d332..9f78bd07ecbf8 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/get_page_data.js +++ b/x-pack/plugins/monitoring/public/views/kibana/instances/get_page_data.js @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; export function getPageData($injector) { const $http = $injector.get('$http'); const globalState = $injector.get('globalState'); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana/instances`; - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.html b/x-pack/plugins/monitoring/public/views/kibana/instances/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.html rename to x-pack/plugins/monitoring/public/views/kibana/instances/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.js b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js similarity index 56% rename from x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.js rename to x-pack/plugins/monitoring/public/views/kibana/instances/index.js index 51a7e033bd0d6..d179928ded693 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.js +++ b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js @@ -5,14 +5,13 @@ */ import React, { Fragment } from 'react'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import { MonitoringViewBaseEuiTableController } from '../../'; import { getPageData } from './get_page_data'; import template from './index.html'; -import { KibanaInstances } from 'plugins/monitoring/components/kibana/instances'; +import { KibanaInstances } from '../../../components/kibana/instances'; import { SetupModeRenderer } from '../../../components/renderers'; -import { I18nContext } from 'ui/i18n'; import { KIBANA_SYSTEM_ID, CODE_PATH_KIBANA } from '../../../../common/constants'; uiRoutes.when('/kibana/instances', { @@ -40,31 +39,29 @@ uiRoutes.when('/kibana/instances', { const renderReact = () => { this.renderReact( - - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> ); }; diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/overview/index.html b/x-pack/plugins/monitoring/public/views/kibana/overview/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/kibana/overview/index.html rename to x-pack/plugins/monitoring/public/views/kibana/overview/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/overview/index.js b/x-pack/plugins/monitoring/public/views/kibana/overview/index.js similarity index 58% rename from x-pack/legacy/plugins/monitoring/public/views/kibana/overview/index.js rename to x-pack/plugins/monitoring/public/views/kibana/overview/index.js index 0705e3b7f270b..b0be4f7862a91 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/kibana/overview/index.js +++ b/x-pack/plugins/monitoring/public/views/kibana/overview/index.js @@ -8,12 +8,12 @@ * Kibana Overview */ import React from 'react'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; +import { uiRoutes } from '../../../angular/helpers/routes'; import { MonitoringTimeseriesContainer } from '../../../components/chart'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../lib/route_init'; import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { Legacy } from '../../../legacy_shims'; import { EuiPage, EuiPageBody, @@ -24,7 +24,6 @@ import { EuiFlexItem, } from '@elastic/eui'; import { ClusterStatus } from '../../../components/kibana/cluster_status'; -import { I18nContext } from 'ui/i18n'; import { MonitoringViewBaseController } from '../../base_controller'; import { CODE_PATH_KIBANA } from '../../../../common/constants'; @@ -32,7 +31,7 @@ function getPageData($injector) { const $http = $injector.get('$http'); const globalState = $injector.get('globalState'); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana`; - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { @@ -79,34 +78,32 @@ uiRoutes.when('/kibana', { } this.renderReact( - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/license/controller.js b/x-pack/plugins/monitoring/public/views/license/controller.js similarity index 71% rename from x-pack/legacy/plugins/monitoring/public/views/license/controller.js rename to x-pack/plugins/monitoring/public/views/license/controller.js index ce6e9c8fb74cd..1d6a179db98c4 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/license/controller.js +++ b/x-pack/plugins/monitoring/public/views/license/controller.js @@ -8,19 +8,17 @@ import { get, find } from 'lodash'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import chrome from 'plugins/monitoring/np_imports/ui/chrome'; +import { Legacy } from '../../legacy_shims'; import { formatDateTimeLocal } from '../../../common/formatting'; -import { MANAGEMENT_BASE_PATH } from 'plugins/xpack_main/components'; -import { License } from 'plugins/monitoring/components'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { I18nContext } from 'ui/i18n'; +import { BASE_PATH as MANAGEMENT_BASE_PATH } from '../../../../../plugins/license_management/common/constants'; +import { License } from '../../components'; const REACT_NODE_ID = 'licenseReact'; export class LicenseViewController { constructor($injector, $scope) { - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); + Legacy.shims.timefilter.disableTimeRangeSelector(); + Legacy.shims.timefilter.disableAutoRefreshSelector(); $scope.$on('$destroy', () => { unmountComponentAtNode(document.getElementById(REACT_NODE_ID)); @@ -47,14 +45,14 @@ export class LicenseViewController { this.isExpired = Date.now() > get(cluster, 'license.expiry_date_in_millis'); this.isPrimaryCluster = cluster.isPrimary; - const basePath = chrome.getBasePath(); + const basePath = Legacy.shims.getBasePath(); this.uploadLicensePath = basePath + '/app/kibana#' + MANAGEMENT_BASE_PATH + 'upload_license'; this.renderReact($scope); } renderReact($scope) { - const injector = chrome.dangerouslyGetActiveInjector(); + const injector = Legacy.shims.getAngularInjector(); const timezone = injector.get('config').get('dateFormat:tz'); $scope.$evalAsync(() => { const { isPrimaryCluster, license, isExpired, uploadLicensePath } = this; @@ -65,16 +63,14 @@ export class LicenseViewController { // Mount the React component to the template render( - - - , + , document.getElementById(REACT_NODE_ID) ); }); diff --git a/x-pack/legacy/plugins/monitoring/public/views/license/index.html b/x-pack/plugins/monitoring/public/views/license/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/license/index.html rename to x-pack/plugins/monitoring/public/views/license/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/license/index.js b/x-pack/plugins/monitoring/public/views/license/index.js similarity index 83% rename from x-pack/legacy/plugins/monitoring/public/views/license/index.js rename to x-pack/plugins/monitoring/public/views/license/index.js index e0796c85d8f85..46e93a8f01f45 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/license/index.js +++ b/x-pack/plugins/monitoring/public/views/license/index.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../angular/helpers/routes'; +import { routeInitProvider } from '../../lib/route_init'; import template from './index.html'; import { LicenseViewController } from './controller'; import { CODE_PATH_LICENSE } from '../../../common/constants'; diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/advanced/index.html b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/node/advanced/index.html rename to x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js similarity index 65% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/node/advanced/index.js rename to x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js index 29cf4839eff94..4099a99f122f2 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js @@ -9,13 +9,13 @@ */ import React from 'react'; import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../../lib/route_init'; import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { Legacy } from '../../../../legacy_shims'; import { MonitoringViewBaseController } from '../../../base_controller'; -import { DetailStatus } from 'plugins/monitoring/components/logstash/detail_status'; +import { DetailStatus } from '../../../../components/logstash/detail_status'; import { EuiPage, EuiPageBody, @@ -26,7 +26,6 @@ import { EuiFlexItem, } from '@elastic/eui'; import { MonitoringTimeseriesContainer } from '../../../../components/chart'; -import { I18nContext } from 'ui/i18n'; import { CODE_PATH_LOGSTASH } from '../../../../../common/constants'; function getPageData($injector) { @@ -34,7 +33,7 @@ function getPageData($injector) { const globalState = $injector.get('globalState'); const $route = $injector.get('$route'); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/node/${$route.current.params.uuid}`; - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { @@ -97,31 +96,29 @@ uiRoutes.when('/logstash/node/:uuid/advanced', { ]; this.renderReact( - - - - - - - - - - {metricsToShow.map((metric, index) => ( - - - - - ))} - - - - - + + + + + + + + + {metricsToShow.map((metric, index) => ( + + + + + ))} + + + + ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/index.html b/x-pack/plugins/monitoring/public/views/logstash/node/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/node/index.html rename to x-pack/plugins/monitoring/public/views/logstash/node/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/index.js similarity index 65% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/node/index.js rename to x-pack/plugins/monitoring/public/views/logstash/node/index.js index f1777d1e46ef0..141761d8cc11a 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/node/index.js @@ -9,12 +9,12 @@ */ import React from 'react'; import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../lib/route_init'; import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { DetailStatus } from 'plugins/monitoring/components/logstash/detail_status'; +import { Legacy } from '../../../legacy_shims'; +import { DetailStatus } from '../../../components/logstash/detail_status'; import { EuiPage, EuiPageBody, @@ -25,7 +25,6 @@ import { EuiFlexItem, } from '@elastic/eui'; import { MonitoringTimeseriesContainer } from '../../../components/chart'; -import { I18nContext } from 'ui/i18n'; import { MonitoringViewBaseController } from '../../base_controller'; import { CODE_PATH_LOGSTASH } from '../../../../common/constants'; @@ -34,7 +33,7 @@ function getPageData($injector) { const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/node/${$route.current.params.uuid}`; - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { @@ -98,31 +97,29 @@ uiRoutes.when('/logstash/node/:uuid', { ]; this.renderReact( - - - - - - - - - - {metricsToShow.map((metric, index) => ( - - - - - ))} - - - - - + + + + + + + + + {metricsToShow.map((metric, index) => ( + + + + + ))} + + + + ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/pipelines/index.html b/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/node/pipelines/index.html rename to x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/pipelines/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.js similarity index 74% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/node/pipelines/index.js rename to x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.js index 017988b70bdd4..442e8533c18f6 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/pipelines/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.js @@ -10,14 +10,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { isPipelineMonitoringSupportedInVersion } from 'plugins/monitoring/lib/logstash/pipelines'; +import { uiRoutes } from '../../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../../lib/route_init'; +import { isPipelineMonitoringSupportedInVersion } from '../../../../lib/logstash/pipelines'; import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { Legacy } from '../../../../legacy_shims'; import { MonitoringViewBaseEuiTableController } from '../../../'; -import { I18nContext } from 'ui/i18n'; import { PipelineListing } from '../../../../components/logstash/pipeline_listing/pipeline_listing'; import { DetailStatus } from '../../../../components/logstash/detail_status'; import { CODE_PATH_LOGSTASH } from '../../../../../common/constants'; @@ -31,7 +30,7 @@ const getPageData = ($injector, _api = undefined, routeOptions = {}) => { const logstashUuid = $route.current.params.uuid; const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/node/${logstashUuid}/pipelines`; - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { @@ -107,23 +106,21 @@ uiRoutes.when('/logstash/node/:uuid/pipelines', { }; this.renderReact( - - - + ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/get_page_data.js b/x-pack/plugins/monitoring/public/views/logstash/nodes/get_page_data.js similarity index 80% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/get_page_data.js rename to x-pack/plugins/monitoring/public/views/logstash/nodes/get_page_data.js index d476f6ba5143e..1d5d6007814f4 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/get_page_data.js +++ b/x-pack/plugins/monitoring/public/views/logstash/nodes/get_page_data.js @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; export function getPageData($injector) { const $http = $injector.get('$http'); const globalState = $injector.get('globalState'); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/nodes`; - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.html b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.html rename to x-pack/plugins/monitoring/public/views/logstash/nodes/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.js b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js similarity index 57% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.js rename to x-pack/plugins/monitoring/public/views/logstash/nodes/index.js index 30f851b2a7534..49b2a20f11ea2 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; import { MonitoringViewBaseEuiTableController } from '../../'; import { getPageData } from './get_page_data'; import template from './index.html'; -import { I18nContext } from 'ui/i18n'; import { Listing } from '../../../components/logstash/listing'; import { SetupModeRenderer } from '../../../components/renderers'; import { CODE_PATH_LOGSTASH, LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; @@ -41,28 +40,26 @@ uiRoutes.when('/logstash/nodes', { () => this.data, data => { this.renderReact( - - ( - - {flyoutComponent} - - {bottomBarComponent} - - )} - /> - + ( + + {flyoutComponent} + + {bottomBarComponent} + + )} + /> ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/overview/index.html b/x-pack/plugins/monitoring/public/views/logstash/overview/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/overview/index.html rename to x-pack/plugins/monitoring/public/views/logstash/overview/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/overview/index.js b/x-pack/plugins/monitoring/public/views/logstash/overview/index.js similarity index 73% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/overview/index.js rename to x-pack/plugins/monitoring/public/views/logstash/overview/index.js index f41f54555952e..05b1747f4cfff 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/overview/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/overview/index.js @@ -8,12 +8,11 @@ * Logstash Overview */ import React from 'react'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../lib/route_init'; import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { I18nContext } from 'ui/i18n'; +import { Legacy } from '../../../legacy_shims'; import { Overview } from '../../../components/logstash/overview'; import { MonitoringViewBaseController } from '../../base_controller'; import { CODE_PATH_LOGSTASH } from '../../../../common/constants'; @@ -22,7 +21,7 @@ function getPageData($injector) { const $http = $injector.get('$http'); const globalState = $injector.get('globalState'); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash`; - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { @@ -63,14 +62,12 @@ uiRoutes.when('/logstash', { () => this.data, data => { this.renderReact( - - - + ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/pipeline/index.html b/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/pipeline/index.html rename to x-pack/plugins/monitoring/public/views/logstash/pipeline/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/pipeline/index.js b/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js similarity index 77% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/pipeline/index.js rename to x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js index 11cb8516847c8..0dd24d68540ce 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/pipeline/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js @@ -8,21 +8,20 @@ * Logstash Node Pipeline View */ import React from 'react'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; +import { uiRoutes } from '../../../angular/helpers/routes'; import moment from 'moment'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../lib/route_init'; import { CALCULATE_DURATION_SINCE, CODE_PATH_LOGSTASH } from '../../../../common/constants'; import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; import template from './index.html'; import { i18n } from '@kbn/i18n'; -import { List } from 'plugins/monitoring/components/logstash/pipeline_viewer/models/list'; -import { PipelineState } from 'plugins/monitoring/components/logstash/pipeline_viewer/models/pipeline_state'; -import { PipelineViewer } from 'plugins/monitoring/components/logstash/pipeline_viewer'; -import { Pipeline } from 'plugins/monitoring/components/logstash/pipeline_viewer/models/pipeline'; -import { vertexFactory } from 'plugins/monitoring/components/logstash/pipeline_viewer/models/graph/vertex_factory'; +import { List } from '../../../components/logstash/pipeline_viewer/models/list'; +import { PipelineState } from '../../../components/logstash/pipeline_viewer/models/pipeline_state'; +import { PipelineViewer } from '../../../components/logstash/pipeline_viewer'; +import { Pipeline } from '../../../components/logstash/pipeline_viewer/models/pipeline'; +import { vertexFactory } from '../../../components/logstash/pipeline_viewer/models/graph/vertex_factory'; import { MonitoringViewBaseController } from '../../base_controller'; -import { I18nContext } from 'ui/i18n'; import { EuiPageBody, EuiPage, EuiPageContent } from '@elastic/eui'; let previousPipelineHash = undefined; @@ -146,22 +145,20 @@ uiRoutes.when('/logstash/pipelines/:id/:hash?', { this.pipelineState = new PipelineState(data.pipeline); this.detailVertex = data.vertex ? vertexFactory(null, data.vertex) : null; this.renderReact( - - - - - - - - - + + + + + + + ); } ); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/pipelines/index.html b/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/pipelines/index.html rename to x-pack/plugins/monitoring/public/views/logstash/pipelines/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/pipelines/index.js b/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js similarity index 75% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/pipelines/index.js rename to x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js index 75a18000c14dd..4ddaba1e0a7c9 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/pipelines/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js @@ -7,13 +7,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { isPipelineMonitoringSupportedInVersion } from 'plugins/monitoring/lib/logstash/pipelines'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../lib/route_init'; +import { isPipelineMonitoringSupportedInVersion } from '../../../lib/logstash/pipelines'; import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { I18nContext } from 'ui/i18n'; +import { Legacy } from '../../../legacy_shims'; import { PipelineListing } from '../../../components/logstash/pipeline_listing/pipeline_listing'; import { MonitoringViewBaseEuiTableController } from '../..'; import { CODE_PATH_LOGSTASH } from '../../../../common/constants'; @@ -29,7 +28,7 @@ const getPageData = ($injector, _api = undefined, routeOptions = {}) => { const Private = $injector.get('Private'); const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/pipelines`; - const timeBounds = timefilter.getBounds(); + const timeBounds = Legacy.shims.timefilter.getBounds(); return $http .post(url, { @@ -103,21 +102,19 @@ uiRoutes.when('/logstash/pipelines', { }; super.renderReact( - - this.onBrush({ xaxis })} - stats={pageData.clusterStatus} - data={pageData.pipelines} - {...this.getPaginationTableProps(pagination)} - upgradeMessage={upgradeMessage} - dateFormat={config.get('dateFormat')} - angular={{ - kbnUrl, - scope: $scope, - }} - /> - + this.onBrush({ xaxis })} + stats={pageData.clusterStatus} + data={pageData.pipelines} + {...this.getPaginationTableProps(pagination)} + upgradeMessage={upgradeMessage} + dateFormat={config.get('dateFormat')} + angular={{ + kbnUrl, + scope: $scope, + }} + /> ); }; diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/__tests__/model_updater.test.js b/x-pack/plugins/monitoring/public/views/no_data/__tests__/model_updater.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/no_data/__tests__/model_updater.test.js rename to x-pack/plugins/monitoring/public/views/no_data/__tests__/model_updater.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js b/x-pack/plugins/monitoring/public/views/no_data/controller.js similarity index 87% rename from x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js rename to x-pack/plugins/monitoring/public/views/no_data/controller.js index a914aa0155e90..14c30da2ce999 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js +++ b/x-pack/plugins/monitoring/public/views/no_data/controller.js @@ -10,17 +10,17 @@ import { NodeSettingsChecker, Enabler, startChecks, -} from 'plugins/monitoring/lib/elasticsearch_settings'; +} from '../../lib/elasticsearch_settings'; import { ModelUpdater } from './model_updater'; -import { NoData } from 'plugins/monitoring/components'; -import { I18nContext } from 'ui/i18n'; +import { NoData } from '../../components'; import { CODE_PATH_LICENSE } from '../../../common/constants'; import { MonitoringViewBaseController } from '../base_controller'; import { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; +import { Legacy } from '../../legacy_shims'; export class NoDataController extends MonitoringViewBaseController { constructor($injector, $scope) { + window.injectorThree = $injector; const monitoringClusters = $injector.get('monitoringClusters'); const kbnUrl = $injector.get('kbnUrl'); const $http = $injector.get('$http'); @@ -99,18 +99,13 @@ export class NoDataController extends MonitoringViewBaseController { render(enabler) { const props = this; - const { cloud } = npSetup.plugins; - const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); - this.renderReact( - - - + ); } } diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/index.html b/x-pack/plugins/monitoring/public/views/no_data/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/no_data/index.html rename to x-pack/plugins/monitoring/public/views/no_data/index.html diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/index.js b/x-pack/plugins/monitoring/public/views/no_data/index.js similarity index 63% rename from x-pack/legacy/plugins/monitoring/public/views/no_data/index.js rename to x-pack/plugins/monitoring/public/views/no_data/index.js index edade513e5ab2..9876739dfcbbe 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/no_data/index.js +++ b/x-pack/plugins/monitoring/public/views/no_data/index.js @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; +import { uiRoutes } from '../../angular/helpers/routes'; import template from './index.html'; import { NoDataController } from './controller'; -uiRoutes - .when('/no-data', { - template, - controller: NoDataController, - }) - .otherwise({ redirectTo: '/home' }); +uiRoutes.when('/no-data', { + template, + controller: NoDataController, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/model_updater.js b/x-pack/plugins/monitoring/public/views/no_data/model_updater.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/no_data/model_updater.js rename to x-pack/plugins/monitoring/public/views/no_data/model_updater.js diff --git a/x-pack/plugins/monitoring/server/index.ts b/x-pack/plugins/monitoring/server/index.ts index a992037fc6087..60f04c535ebf1 100644 --- a/x-pack/plugins/monitoring/server/index.ts +++ b/x-pack/plugins/monitoring/server/index.ts @@ -10,8 +10,14 @@ import { Plugin } from './plugin'; import { configSchema } from './config'; import { deprecations } from './deprecations'; +export { MonitoringConfig } from './config'; export const plugin = (initContext: PluginInitializerContext) => new Plugin(initContext); export const config: PluginConfigDescriptor> = { schema: configSchema, deprecations, + exposeToBrowser: { + enabled: true, + ui: true, + kibana: true, + }, }; diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index 90187b7853185..3df2b015aa034 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -8,7 +8,7 @@ import { CallAPIOptions } from 'src/core/server'; import { take } from 'rxjs/operators'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { Observable } from 'rxjs'; -import { KIBANA_STATS_TYPE_MONITORING } from '../../../../legacy/plugins/monitoring/common/constants'; +import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants'; import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants'; import { PluginsSetup } from '../plugin'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bf257d66a59bc..4cd152f2d297d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11921,7 +11921,6 @@ "xpack.monitoring.metrics.logstashInstance.systemLoad.last5MinutesDescription": "過去 5 分間の平均負荷です。", "xpack.monitoring.metrics.logstashInstance.systemLoad.last5MinutesLabel": "5m", "xpack.monitoring.monitoringDescription": "Elastic Stack のリアルタイムのヘルスとパフォーマンスをトラッキングします。", - "xpack.monitoring.monitoringTitle": "Monitoring", "xpack.monitoring.noData.blurbs.changesNeededDescription": "監視を実行するには、次の手順に従います", "xpack.monitoring.noData.blurbs.changesNeededTitle": "調整が必要です", "xpack.monitoring.noData.blurbs.cloudDeploymentDescription": "次の場所に戻ってください: ", @@ -12001,7 +12000,6 @@ "xpack.monitoring.summaryStatus.statusDescription": "ステータス", "xpack.monitoring.summaryStatus.statusIconLabel": "ステータス: {status}", "xpack.monitoring.summaryStatus.statusIconTitle": "ステータス: {statusIcon}", - "xpack.monitoring.uiExportsDescription": "Elastic Stack の監視です", "xpack.painlessLab.apiReferenceButtonLabel": "API リファレンス", "xpack.painlessLab.context.defaultLabel": "スクリプト結果は文字列に変換されます", "xpack.painlessLab.context.filterLabel": "フィルターのスクリプトクエリのコンテキストを使用する", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 323ddfda32d70..619ea0646eb44 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11925,7 +11925,6 @@ "xpack.monitoring.metrics.logstashInstance.systemLoad.last5MinutesDescription": "过去 5 分钟的负载平均值。", "xpack.monitoring.metrics.logstashInstance.systemLoad.last5MinutesLabel": "5 分钟", "xpack.monitoring.monitoringDescription": "跟踪 Elastic Stack 的实时运行状况和性能。", - "xpack.monitoring.monitoringTitle": "Monitoring", "xpack.monitoring.noData.blurbs.changesNeededDescription": "要运行 Monitoring,请执行以下步骤", "xpack.monitoring.noData.blurbs.changesNeededTitle": "您需要做些调整", "xpack.monitoring.noData.blurbs.cloudDeploymentDescription": "请返回到您的 ", @@ -12005,7 +12004,6 @@ "xpack.monitoring.summaryStatus.statusDescription": "状态", "xpack.monitoring.summaryStatus.statusIconLabel": "状态:{status}", "xpack.monitoring.summaryStatus.statusIconTitle": "状态:{statusIcon}", - "xpack.monitoring.uiExportsDescription": "Elastic Stack 的 Monitoring 组件", "xpack.painlessLab.apiReferenceButtonLabel": "API 参考", "xpack.painlessLab.context.defaultLabel": "脚本结果将转换成字符串", "xpack.painlessLab.context.filterLabel": "使用筛选脚本查询的上下文", From 16ba937bae63c9925b80285ad076325ee1a63569 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Thu, 30 Apr 2020 13:08:50 -0700 Subject: [PATCH 052/122] [APM] Client new platform migration (#64046) * migrate files from legacy path to new plugin path * update file paths to reflect migrated files * move minimal legacy client files back to legacy path in order to run kibana * Completes the full cutover to the new kibana platform removing all shims and legacy adapters. * Adds APM to ignored list for casing check. * - moves public/utils/pickKeys.ts to common/utils/pick_keys.ts - exposes getTraceUrl as a plugin static export of apm/public and updates import in infra - fixes FeaturesPluginSetup import in apm/public app - renames get_apm_index_pattern_titles -> get_apm_index_pattern_title - getApmIndexPatternTitle is now a synchronous getter function - removes unused comments and xpack.apm.apmForESDescription i18n translations * Moves automatic index pattern saved object creation from plugin start to when the Home screen first renders * removed unnecessary legacy css imports * fixed ci issues by: - moving readOnly badge, and help extension setup to occure only when apm app is mounted - registering saved object types - also moved createStaticIndexPattern from a react useEffect on the APM home screen to when the app is mounted --- src/cli/cluster/cluster_manager.ts | 4 +- src/dev/precommit_hook/casing_check_config.js | 6 +- src/dev/run_check_lockfile_symlinks.js | 4 +- src/dev/storybook/aliases.ts | 2 +- src/dev/typescript/projects.ts | 2 +- x-pack/index.js | 2 - x-pack/legacy/plugins/apm/.prettierrc | 4 - .../apm/e2e/cypress/integration/snapshots.js | 10 - x-pack/legacy/plugins/apm/index.ts | 166 ---- x-pack/legacy/plugins/apm/mappings.json | 933 ------------------ x-pack/legacy/plugins/apm/public/index.scss | 16 - x-pack/legacy/plugins/apm/public/index.tsx | 36 - .../apm/public/legacy_register_feature.ts | 24 - .../getConfigFromInjectedMetadata.ts | 24 - .../plugins/apm/public/new-platform/index.tsx | 13 - .../apm/public/new-platform/plugin.tsx | 203 ---- .../apm/public/style/global_overrides.css | 34 - .../plugins/apm/public/templates/index.html | 1 - .../{legacy => }/plugins/apm/CONTRIBUTING.md | 0 .../apm/common/apm_saved_object_constants.ts | 2 +- .../apm/common/utils/pick_keys.ts} | 0 .../plugins/apm/dev_docs/github_commands.md | 0 .../plugins/apm/dev_docs/typescript.md | 4 +- .../plugins/apm/dev_docs/vscode_setup.md | 6 +- .../{legacy => }/plugins/apm/e2e/.gitignore | 0 x-pack/{legacy => }/plugins/apm/e2e/README.md | 6 +- .../plugins/apm/e2e/ci/Dockerfile | 0 .../plugins/apm/e2e/ci/entrypoint.sh | 4 +- .../plugins/apm/e2e/ci/kibana.e2e.yml | 0 .../plugins/apm/e2e/ci/prepare-kibana.sh | 2 +- .../{legacy => }/plugins/apm/e2e/cypress.json | 0 .../apm/e2e/cypress/fixtures/example.json | 0 .../apm/e2e/cypress/integration/apm.feature | 0 .../apm/e2e/cypress/integration/helpers.ts | 0 .../apm/e2e/cypress/integration/snapshots.js | 16 + .../plugins/apm/e2e/cypress/plugins/index.js | 0 .../apm/e2e/cypress/support/commands.js | 0 .../plugins/apm/e2e/cypress/support/index.ts | 0 .../cypress/support/step_definitions/apm.ts | 0 .../apm/e2e/cypress/typings/index.d.ts | 0 .../plugins/apm/e2e/cypress/webpack.config.js | 0 .../plugins/apm/e2e/ingest-data/replay.js | 0 .../{legacy => }/plugins/apm/e2e/package.json | 0 .../{legacy => }/plugins/apm/e2e/run-e2e.sh | 2 +- .../plugins/apm/e2e/tsconfig.json | 0 x-pack/{legacy => }/plugins/apm/e2e/yarn.lock | 0 x-pack/plugins/apm/kibana.json | 24 +- .../plugins/apm/public/application/index.tsx | 123 +++ .../__test__/APMIndicesPermission.test.tsx | 0 .../app/APMIndicesPermission/index.tsx | 0 .../DetailView/ErrorTabs.tsx | 2 +- .../DetailView/ExceptionStacktrace.test.tsx | 0 .../DetailView/ExceptionStacktrace.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 0 .../DetailView/index.test.tsx | 0 .../ErrorGroupDetails/DetailView/index.tsx | 4 +- .../ErrorGroupDetails/Distribution/index.tsx | 0 .../app/ErrorGroupDetails/index.tsx | 4 +- .../List/__test__/List.test.tsx | 0 .../__test__/__snapshots__/List.test.tsx.snap | 0 .../List/__test__/props.json | 0 .../app/ErrorGroupOverview/List/index.tsx | 4 +- .../app/ErrorGroupOverview/index.tsx | 4 +- .../public/components/app/Home/Home.test.tsx | 0 .../app/Home/__snapshots__/Home.test.tsx.snap | 2 - .../apm/public/components/app/Home/index.tsx | 0 .../app/Main/ProvideBreadcrumbs.tsx | 0 .../app/Main/ScrollToTopOnPathChange.tsx | 0 .../app/Main/UpdateBreadcrumbs.test.tsx | 0 .../components/app/Main/UpdateBreadcrumbs.tsx | 0 .../UpdateBreadcrumbs.test.tsx.snap | 0 .../Main/__test__/ProvideBreadcrumbs.test.tsx | 0 .../app/Main/route_config/index.tsx | 4 +- .../route_handlers/agent_configuration.tsx | 0 .../app/Main/route_config/route_names.tsx | 0 .../AlertingFlyout/index.tsx | 4 +- .../AlertIntegrations/index.tsx | 2 +- .../app/ServiceDetails/ServiceDetailTabs.tsx | 5 +- .../TransactionSelect.tsx | 0 .../MachineLearningFlyout/index.tsx | 2 +- .../MachineLearningFlyout/view.tsx | 0 .../ServiceIntegrations/WatcherFlyout.tsx | 71 +- .../createErrorGroupWatch.test.ts.snap | 0 .../__test__/createErrorGroupWatch.test.ts | 0 .../__test__/esResponse.ts | 0 .../createErrorGroupWatch.ts | 2 +- .../ServiceIntegrations/index.tsx | 0 .../components/app/ServiceDetails/index.tsx | 0 .../components/app/ServiceMap/BetaBadge.tsx | 0 .../components/app/ServiceMap/Controls.tsx | 0 .../app/ServiceMap/Cytoscape.stories.tsx | 0 .../components/app/ServiceMap/Cytoscape.tsx | 2 +- .../app/ServiceMap/EmptyBanner.test.tsx | 0 .../components/app/ServiceMap/EmptyBanner.tsx | 0 .../app/ServiceMap/LoadingOverlay.tsx | 0 .../app/ServiceMap/Popover/Buttons.tsx | 0 .../app/ServiceMap/Popover/Contents.tsx | 2 +- .../app/ServiceMap/Popover/Info.tsx | 2 +- .../ServiceMap/Popover/Popover.stories.tsx | 0 .../Popover/ServiceMetricFetcher.tsx | 2 +- .../ServiceMap/Popover/ServiceMetricList.tsx | 2 +- .../app/ServiceMap/Popover/index.tsx | 2 +- .../cytoscape-layout-test-response.json | 0 .../app/ServiceMap/cytoscapeOptions.ts | 2 +- .../public/components/app/ServiceMap/icons.ts | 4 +- .../components/app/ServiceMap/icons/aws.svg | 0 .../app/ServiceMap/icons/cassandra.svg | 0 .../components/app/ServiceMap/icons/dark.svg | 0 .../app/ServiceMap/icons/database.svg | 0 .../app/ServiceMap/icons/default.svg | 0 .../app/ServiceMap/icons/documents.svg | 0 .../app/ServiceMap/icons/dot-net.svg | 0 .../app/ServiceMap/icons/elasticsearch.svg | 0 .../components/app/ServiceMap/icons/globe.svg | 0 .../components/app/ServiceMap/icons/go.svg | 0 .../app/ServiceMap/icons/graphql.svg | 0 .../components/app/ServiceMap/icons/grpc.svg | 0 .../app/ServiceMap/icons/handlebars.svg | 0 .../components/app/ServiceMap/icons/java.svg | 0 .../components/app/ServiceMap/icons/kafka.svg | 0 .../app/ServiceMap/icons/mongodb.svg | 0 .../components/app/ServiceMap/icons/mysql.svg | 0 .../app/ServiceMap/icons/nodejs.svg | 0 .../components/app/ServiceMap/icons/php.svg | 0 .../app/ServiceMap/icons/postgresql.svg | 0 .../app/ServiceMap/icons/python.svg | 0 .../components/app/ServiceMap/icons/redis.svg | 0 .../components/app/ServiceMap/icons/ruby.svg | 0 .../components/app/ServiceMap/icons/rumjs.svg | 0 .../app/ServiceMap/icons/websocket.svg | 0 .../components/app/ServiceMap/index.test.tsx | 2 +- .../components/app/ServiceMap/index.tsx | 4 +- .../app/ServiceMap/useRefDimensions.ts | 0 .../components/app/ServiceMetrics/index.tsx | 2 +- .../app/ServiceNodeMetrics/index.test.tsx | 0 .../app/ServiceNodeMetrics/index.tsx | 2 +- .../app/ServiceNodeOverview/index.tsx | 6 +- .../app/ServiceOverview/NoServicesMessage.tsx | 0 .../ServiceList/__test__/List.test.js | 0 .../__test__/__snapshots__/List.test.js.snap | 0 .../ServiceList/__test__/props.json | 0 .../app/ServiceOverview/ServiceList/index.tsx | 4 +- .../__test__/NoServicesMessage.test.tsx | 0 .../__test__/ServiceOverview.test.tsx | 0 .../NoServicesMessage.test.tsx.snap | 0 .../ServiceOverview.test.tsx.snap | 0 .../components/app/ServiceOverview/index.tsx | 6 +- .../ServicePage/FormRowSelect.tsx | 0 .../ServicePage/ServicePage.tsx | 4 +- .../SettingsPage/SettingFormRow.tsx | 6 +- .../SettingsPage/SettingsPage.tsx | 10 +- .../SettingsPage/saveConfig.ts | 4 +- .../index.stories.tsx | 2 +- .../AgentConfigurationCreateEdit/index.tsx | 2 +- .../List/ConfirmDeleteModal.tsx | 4 +- .../AgentConfigurations/List/index.tsx | 4 +- .../Settings/AgentConfigurations/index.tsx | 2 +- .../app/Settings/ApmIndices/index.test.tsx | 0 .../app/Settings/ApmIndices/index.tsx | 0 .../CustomLink/CreateCustomLinkButton.tsx | 0 .../CustomLinkFlyout/DeleteButton.tsx | 0 .../CustomLinkFlyout/Documentation.tsx | 0 .../CustomLinkFlyout/FiltersSection.tsx | 2 +- .../CustomLinkFlyout/FlyoutFooter.tsx | 0 .../CustomLinkFlyout/LinkPreview.test.tsx | 0 .../CustomLinkFlyout/LinkPreview.tsx | 4 +- .../CustomLinkFlyout/LinkSection.tsx | 2 +- .../CustomLinkFlyout/helper.test.ts | 2 +- .../CustomLink/CustomLinkFlyout/helper.ts | 6 +- .../CustomLink/CustomLinkFlyout/index.tsx | 2 +- .../CustomLinkFlyout/saveCustomLink.ts | 2 +- .../CustomLink/CustomLinkTable.tsx | 2 +- .../CustomizeUI/CustomLink/EmptyPrompt.tsx | 0 .../Settings/CustomizeUI/CustomLink/Title.tsx | 0 .../CustomizeUI/CustomLink/index.test.tsx | 2 +- .../Settings/CustomizeUI/CustomLink/index.tsx | 2 +- .../app/Settings/CustomizeUI/index.tsx | 0 .../public/components/app/Settings/index.tsx | 0 .../app/TraceLink/__test__/TraceLink.test.tsx | 0 .../public/components/app/TraceLink/index.tsx | 4 +- .../app/TraceOverview/TraceList.tsx | 2 +- .../components/app/TraceOverview/index.tsx | 4 +- .../__test__/distribution.test.ts | 2 +- .../TransactionDetails/Distribution/index.tsx | 4 +- .../WaterfallWithSummmary/ErrorCount.tsx | 0 .../MaybeViewTraceLink.tsx | 2 +- .../WaterfallWithSummmary/PercentOfParent.tsx | 0 .../WaterfallWithSummmary/TransactionTabs.tsx | 2 +- .../Marks/__test__/get_agent_marks.test.ts | 2 +- .../Marks/__test__/get_error_marks.test.ts | 0 .../Marks/get_agent_marks.ts | 2 +- .../Marks/get_error_marks.ts | 2 +- .../WaterfallContainer/Marks/index.ts | 0 .../WaterfallContainer/ServiceLegends.tsx | 0 .../Waterfall/FlyoutTopLevelProperties.tsx | 4 +- .../Waterfall/ResponsiveFlyout.tsx | 0 .../Waterfall/SpanFlyout/DatabaseContext.tsx | 2 +- .../Waterfall/SpanFlyout/HttpContext.tsx | 2 +- .../SpanFlyout/StickySpanProperties.tsx | 8 +- .../SpanFlyout/TruncateHeightSection.tsx | 0 .../Waterfall/SpanFlyout/index.tsx | 4 +- .../Waterfall/SyncBadge.stories.tsx | 0 .../Waterfall/SyncBadge.tsx | 0 .../TransactionFlyout/DroppedSpansWarning.tsx | 2 +- .../Waterfall/TransactionFlyout/index.tsx | 2 +- .../Waterfall/WaterfallFlyout.tsx | 0 .../Waterfall/WaterfallItem.tsx | 4 +- .../WaterfallContainer/Waterfall/index.tsx | 0 .../waterfall_helpers.test.ts.snap | 0 .../mock_responses/spans.json | 0 .../mock_responses/transaction.json | 0 .../waterfall_helpers.test.ts | 6 +- .../waterfall_helpers/waterfall_helpers.ts | 8 +- .../WaterfallContainer.stories.tsx | 2 +- .../WaterfallContainer/index.tsx | 0 .../waterfallContainer.stories.data.ts | 0 .../__tests__/ErrorCount.test.tsx | 0 .../WaterfallWithSummmary/index.tsx | 2 +- .../app/TransactionDetails/index.tsx | 4 +- .../app/TransactionOverview/List/index.tsx | 4 +- .../__jest__/TransactionOverview.test.tsx | 0 .../app/TransactionOverview/index.tsx | 4 +- .../app/TransactionOverview/useRedirect.ts | 0 .../components/shared/ApmHeader/index.tsx | 0 .../DatePicker/__test__/DatePicker.test.tsx | 0 .../components/shared/DatePicker/index.tsx | 0 .../public/components/shared/EmptyMessage.tsx | 0 .../shared/EnvironmentBadge/index.tsx | 0 .../shared/EnvironmentFilter/index.tsx | 2 +- .../ErrorRateAlertTrigger/index.stories.tsx | 0 .../shared/ErrorRateAlertTrigger/index.tsx | 4 +- .../components/shared/ErrorStatePrompt.tsx | 0 .../public/components/shared/EuiTabLink.tsx | 0 .../shared/HeightRetainer/index.tsx | 0 .../ImpactBar/__test__/ImpactBar.test.js | 0 .../__snapshots__/ImpactBar.test.js.snap | 0 .../components/shared/ImpactBar/index.tsx | 0 .../shared/KeyValueTable/FormattedValue.tsx | 2 +- .../__test__/KeyValueTable.test.tsx | 0 .../components/shared/KeyValueTable/index.tsx | 0 .../shared/KueryBar/Typeahead/ClickOutside.js | 1 + .../shared/KueryBar/Typeahead/Suggestion.js | 0 .../shared/KueryBar/Typeahead/Suggestions.js | 0 .../shared/KueryBar/Typeahead/index.js | 0 .../shared/KueryBar/get_bool_filter.ts | 4 +- .../components/shared/KueryBar/index.tsx | 2 +- .../LicensePrompt/LicensePrompt.stories.tsx | 0 .../components/shared/LicensePrompt/index.tsx | 0 .../Links/DiscoverLinks/DiscoverErrorLink.tsx | 4 +- .../Links/DiscoverLinks/DiscoverLink.tsx | 2 +- .../Links/DiscoverLinks/DiscoverSpanLink.tsx | 4 +- .../DiscoverLinks/DiscoverTransactionLink.tsx | 4 +- .../__test__/DiscoverErrorButton.test.tsx | 2 +- .../__test__/DiscoverErrorLink.test.tsx | 2 +- .../DiscoverLinks.integration.test.tsx | 6 +- .../DiscoverTransactionButton.test.tsx | 2 +- .../__test__/DiscoverTransactionLink.test.tsx | 2 +- .../DiscoverErrorButton.test.tsx.snap | 0 .../DiscoverErrorLink.test.tsx.snap | 0 .../DiscoverTransactionButton.test.tsx.snap | 0 .../DiscoverTransactionLink.test.tsx.snap | 0 .../__test__/mockTransaction.json | 0 .../shared/Links/ElasticDocsLink.tsx | 0 .../shared/Links/InfraLink.test.tsx | 0 .../components/shared/Links/InfraLink.tsx | 2 +- .../shared/Links/KibanaLink.test.tsx | 0 .../components/shared/Links/KibanaLink.tsx | 0 .../MachineLearningLinks/MLJobLink.test.tsx | 0 .../Links/MachineLearningLinks/MLJobLink.tsx | 2 +- .../MachineLearningLinks/MLLink.test.tsx | 0 .../Links/MachineLearningLinks/MLLink.tsx | 0 .../shared/Links/SetupInstructionsLink.tsx | 0 .../shared/Links/apm/APMLink.test.tsx | 0 .../components/shared/Links/apm/APMLink.tsx | 0 .../shared/Links/apm/ErrorDetailLink.tsx | 0 .../shared/Links/apm/ErrorOverviewLink.tsx | 2 +- .../shared/Links/apm/ExternalLinks.test.ts | 0 .../shared/Links/apm/ExternalLinks.ts | 0 .../components/shared/Links/apm/HomeLink.tsx | 0 .../shared/Links/apm/MetricOverviewLink.tsx | 2 +- .../shared/Links/apm/ServiceMapLink.tsx | 0 .../apm/ServiceNodeMetricOverviewLink.tsx | 2 +- .../Links/apm/ServiceNodeOverviewLink.tsx | 2 +- .../shared/Links/apm/ServiceOverviewLink.tsx | 2 +- .../shared/Links/apm/SettingsLink.tsx | 0 .../shared/Links/apm/TraceOverviewLink.tsx | 2 +- .../Links/apm/TransactionDetailLink.tsx | 2 +- .../Links/apm/TransactionOverviewLink.tsx | 2 +- .../Links/apm/agentConfigurationLinks.tsx | 2 +- .../components/shared/Links/rison_helpers.ts | 0 .../shared/Links/url_helpers.test.tsx | 0 .../components/shared/Links/url_helpers.ts | 4 +- .../components/shared/LoadingStatePrompt.tsx | 0 .../LocalUIFilters/Filter/FilterBadgeList.tsx | 0 .../Filter/FilterTitleButton.tsx | 0 .../shared/LocalUIFilters/Filter/index.tsx | 0 .../TransactionTypeFilter/index.tsx | 0 .../shared/LocalUIFilters/index.tsx | 4 +- .../__test__/ManagedTable.test.js | 0 .../__snapshots__/ManagedTable.test.js.snap | 0 .../components/shared/ManagedTable/index.tsx | 0 .../__test__/ErrorMetadata.test.tsx | 2 +- .../MetadataTable/ErrorMetadata/index.tsx | 2 +- .../MetadataTable/ErrorMetadata/sections.ts | 0 .../shared/MetadataTable/Section.tsx | 0 .../__test__/SpanMetadata.test.tsx | 2 +- .../MetadataTable/SpanMetadata/index.tsx | 2 +- .../MetadataTable/SpanMetadata/sections.ts | 0 .../__test__/TransactionMetadata.test.tsx | 2 +- .../TransactionMetadata/index.tsx | 2 +- .../TransactionMetadata/sections.ts | 0 .../__test__/MetadataTable.test.tsx | 0 .../MetadataTable/__test__/Section.test.tsx | 0 .../MetadataTable/__test__/helper.test.ts | 2 +- .../components/shared/MetadataTable/helper.ts | 6 +- .../components/shared/MetadataTable/index.tsx | 0 .../shared/MetadataTable/sections.ts | 0 .../shared/SelectWithPlaceholder/index.tsx | 0 .../PopoverExpression/index.tsx | 0 .../shared/ServiceAlertTrigger/index.tsx | 0 .../Stacktrace/CauseStacktrace.test.tsx | 0 .../shared/Stacktrace/CauseStacktrace.tsx | 2 +- .../components/shared/Stacktrace/Context.tsx | 2 +- .../shared/Stacktrace/FrameHeading.tsx | 2 +- .../Stacktrace/LibraryStacktrace.test.tsx | 0 .../shared/Stacktrace/LibraryStacktrace.tsx | 2 +- .../shared/Stacktrace/Stackframe.tsx | 2 +- .../shared/Stacktrace/Variables.tsx | 2 +- .../Stacktrace/__test__/Stackframe.test.tsx | 2 +- .../__snapshots__/Stackframe.test.tsx.snap | 0 .../__test__/__snapshots__/index.test.ts.snap | 0 .../shared/Stacktrace/__test__/index.test.ts | 2 +- .../Stacktrace/__test__/stacktraces.json | 0 .../components/shared/Stacktrace/index.tsx | 2 +- .../StickyProperties/StickyProperties.test.js | 5 +- .../StickyProperties.test.js.snap | 0 .../shared/StickyProperties/index.tsx | 0 .../shared/Summary/DurationSummaryItem.tsx | 0 .../Summary/ErrorCountSummaryItemBadge.tsx | 0 .../__test__/HttpInfoSummaryItem.test.tsx | 0 .../Summary/HttpInfoSummaryItem/index.tsx | 0 .../__test__/HttpStatusBadge.test.tsx | 0 .../shared/Summary/HttpStatusBadge/index.tsx | 0 .../Summary/HttpStatusBadge/statusCodes.ts | 0 .../Summary/TransactionResultSummaryItem.tsx | 0 .../Summary/TransactionSummary.test.tsx | 0 .../shared/Summary/TransactionSummary.tsx | 4 +- .../Summary/UserAgentSummaryItem.test.tsx | 0 .../shared/Summary/UserAgentSummaryItem.tsx | 2 +- .../Summary/__fixtures__/transactions.ts | 2 +- .../ErrorCountSummaryItemBadge.test.tsx | 0 .../components/shared/Summary/index.tsx | 2 +- .../TimestampTooltip/__test__/index.test.tsx | 0 .../shared/TimestampTooltip/index.tsx | 0 .../CustomLink/CustomLinkPopover.test.tsx | 4 +- .../CustomLink/CustomLinkPopover.tsx | 4 +- .../CustomLink/CustomLinkSection.test.tsx | 4 +- .../CustomLink/CustomLinkSection.tsx | 4 +- .../CustomLink/ManageCustomLink.test.tsx | 0 .../CustomLink/ManageCustomLink.tsx | 0 .../CustomLink/index.test.tsx | 4 +- .../CustomLink/index.tsx | 6 +- .../TransactionActionMenu.tsx | 6 +- .../__test__/TransactionActionMenu.test.tsx | 4 +- .../TransactionActionMenu.test.tsx.snap | 0 .../__test__/mockData.ts | 0 .../__test__/sections.test.ts | 2 +- .../shared/TransactionActionMenu/sections.ts | 2 +- .../TransactionBreakdownGraph/index.tsx | 11 +- .../TransactionBreakdownHeader.tsx | 0 .../TransactionBreakdownKpiList.tsx | 5 +- .../shared/TransactionBreakdown/index.tsx | 2 +- .../index.stories.tsx | 0 .../TransactionDurationAlertTrigger/index.tsx | 4 +- .../charts/CustomPlot/AnnotationsPlot.tsx | 4 +- .../charts/CustomPlot/InteractivePlot.js | 0 .../shared/charts/CustomPlot/Legends.js | 0 .../charts/CustomPlot/SelectionMarker.js | 0 .../shared/charts/CustomPlot/StaticPlot.js | 0 .../shared/charts/CustomPlot/StatusText.js | 0 .../shared/charts/CustomPlot/VoronoiPlot.js | 0 .../charts/CustomPlot/getEmptySeries.ts | 0 .../CustomPlot/getTimezoneOffsetInMs.test.ts | 0 .../CustomPlot/getTimezoneOffsetInMs.ts | 0 .../shared/charts/CustomPlot/index.js | 0 .../charts/CustomPlot/plotUtils.test.ts | 5 +- .../shared/charts/CustomPlot/plotUtils.tsx | 5 +- .../charts/CustomPlot/test/CustomPlot.test.js | 0 .../__snapshots__/CustomPlot.test.js.snap | 0 .../CustomPlot/test/responseWithData.json | 0 .../shared/charts/Histogram/SingleRect.js | 0 .../Histogram/__test__/Histogram.test.js | 0 .../__snapshots__/Histogram.test.js.snap | 0 .../charts/Histogram/__test__/response.json | 0 .../shared/charts/Histogram/index.js | 0 .../components/shared/charts/Legend/index.tsx | 0 .../shared/charts/MetricsChart/index.tsx | 6 +- .../shared/charts/Timeline/LastTickValue.tsx | 0 .../charts/Timeline/Marker/AgentMarker.tsx | 0 .../charts/Timeline/Marker/ErrorMarker.tsx | 2 +- .../Marker/__test__/AgentMarker.test.tsx | 0 .../Marker/__test__/ErrorMarker.test.tsx | 0 .../Timeline/Marker/__test__/Marker.test.tsx | 0 .../__snapshots__/AgentMarker.test.tsx.snap | 0 .../__snapshots__/ErrorMarker.test.tsx.snap | 0 .../__snapshots__/Marker.test.tsx.snap | 0 .../shared/charts/Timeline/Marker/index.tsx | 0 .../shared/charts/Timeline/Timeline.test.tsx | 0 .../shared/charts/Timeline/TimelineAxis.tsx | 0 .../shared/charts/Timeline/VerticalLines.tsx | 0 .../__snapshots__/Timeline.test.tsx.snap | 0 .../shared/charts/Timeline/index.tsx | 0 .../shared/charts/Timeline/plotUtils.ts | 0 .../components/shared/charts/Tooltip/index.js | 0 .../BrowserLineChart.test.tsx | 0 .../TransactionCharts/BrowserLineChart.tsx | 0 .../ChoroplethMap/ChoroplethToolTip.tsx | 0 .../TransactionCharts/ChoroplethMap/index.tsx | 0 .../DurationByCountryMap/index.tsx | 0 .../TransactionLineChart/index.tsx | 2 +- .../shared/charts/TransactionCharts/index.tsx | 9 +- .../charts/helper/__test__/timezone.test.ts | 0 .../shared/charts/helper/timezone.ts | 0 .../Delayed/index.test.tsx | 0 .../useDelayedVisibility/Delayed/index.ts | 0 .../useDelayedVisibility/index.test.tsx | 0 .../shared/useDelayedVisibility/index.ts | 0 .../ApmPluginContext/MockApmPluginContext.tsx | 3 +- .../public/context/ApmPluginContext/index.tsx | 3 +- .../apm/public/context/ChartsSyncContext.tsx | 0 .../InvalidLicenseNotification.tsx | 0 .../public/context/LicenseContext/index.tsx | 2 +- .../context/LoadingIndicatorContext.tsx | 0 .../apm/public/context/LocationContext.tsx | 0 .../public/context/MatchedRouteContext.tsx | 0 .../MockUrlParamsContextProvider.tsx | 0 .../__tests__/UrlParamsContext.test.tsx | 0 .../context/UrlParamsContext/constants.ts | 0 .../context/UrlParamsContext/helpers.ts | 2 +- .../public/context/UrlParamsContext/index.tsx | 6 +- .../UrlParamsContext/resolveUrlParams.ts | 4 +- .../public/context/UrlParamsContext/types.ts | 4 +- .../apm/public}/featureCatalogueEntry.ts | 2 +- .../plugins/apm/public/hooks/useAgentName.ts | 0 .../apm/public/hooks/useApmPluginContext.ts | 0 .../hooks/useAvgDurationByBrowser.test.ts | 0 .../public/hooks/useAvgDurationByBrowser.ts | 6 +- .../public/hooks/useAvgDurationByCountry.ts | 0 .../plugins/apm/public/hooks/useCallApi.ts | 0 .../apm/public/hooks/useChartsSync.tsx | 0 .../apm/public/hooks/useComponentId.tsx | 0 .../apm/public/hooks/useDeepObjectIdentity.ts | 0 .../public/hooks/useDynamicIndexPattern.ts | 2 +- .../hooks/useFetcher.integration.test.tsx | 0 .../apm/public/hooks/useFetcher.test.tsx | 0 .../plugins/apm/public/hooks/useFetcher.tsx | 2 +- .../plugins/apm/public/hooks/useKibanaUrl.ts | 0 .../plugins/apm/public/hooks/useLicense.ts | 0 .../apm/public/hooks/useLoadingIndicator.ts | 0 .../apm/public/hooks/useLocalUIFilters.ts | 8 +- .../plugins/apm/public/hooks/useLocation.tsx | 0 .../apm/public/hooks/useMatchedRoutes.tsx | 0 .../public/hooks/useServiceMetricCharts.ts | 2 +- .../hooks/useServiceTransactionTypes.tsx | 0 .../public/hooks/useTransactionBreakdown.ts | 0 .../apm/public/hooks/useTransactionCharts.ts | 0 .../hooks/useTransactionDistribution.ts | 2 +- .../apm/public/hooks/useTransactionList.ts | 2 +- .../plugins/apm/public/hooks/useUrlParams.tsx | 0 .../plugins/apm/public/hooks/useWaterfall.ts | 0 .../{legacy => }/plugins/apm/public/icon.svg | 0 .../apm-ml-anomaly-detection-example.png | Bin x-pack/plugins/apm/public/index.ts | 25 + x-pack/plugins/apm/public/plugin.ts | 133 +++ .../__tests__/chartSelectors.test.ts | 0 .../__tests__/mockData/anomalyData.ts | 0 .../apm/public/selectors/chartSelectors.ts | 6 +- .../services/__test__/SessionStorageMock.ts | 0 .../public/services/__test__/callApi.test.ts | 0 .../services/__test__/callApmApi.test.ts | 0 .../apm/public/services/rest/callApi.ts | 0 .../public/services/rest/createCallApmApi.ts | 4 +- .../apm/public/services/rest/index_pattern.ts | 6 + .../plugins/apm/public/services/rest/ml.ts | 6 +- .../apm/public/services/rest/watcher.ts | 0 .../apm/public}/setHelpExtension.ts | 0 .../plugins/apm/public/style/variables.ts | 0 .../apm/public}/toggleAppLinkInNav.ts | 2 +- .../apm/public}/updateBadge.ts | 1 - .../utils/__test__/flattenObject.test.ts | 0 .../plugins/apm/public/utils/flattenObject.ts | 2 +- .../formatters/__test__/datetime.test.ts | 0 .../formatters/__test__/duration.test.ts | 0 .../formatters/__test__/formatters.test.ts | 0 .../utils/formatters/__test__/size.test.ts | 0 .../apm/public/utils/formatters/datetime.ts | 0 .../apm/public/utils/formatters/duration.ts | 4 +- .../apm/public/utils/formatters/formatters.ts | 0 .../apm/public/utils/formatters/index.ts | 0 .../apm/public/utils/formatters/size.ts | 2 +- .../public/utils/getRangeFromTimeSeries.ts | 2 +- .../plugins/apm/public/utils/history.ts | 0 .../apm/public/utils/httpStatusCodeToColor.ts | 0 .../public/utils/isValidCoordinateValue.ts | 2 +- .../plugins/apm/public/utils/testHelpers.tsx | 6 +- x-pack/{legacy => }/plugins/apm/readme.md | 8 +- .../plugins/apm/scripts/.gitignore | 0 .../setup-custom-kibana-user-role.ts | 0 .../plugins/apm/scripts/optimize-tsconfig.js | 0 .../apm/scripts/optimize-tsconfig/optimize.js | 0 .../apm/scripts/optimize-tsconfig/paths.js | 0 .../scripts/optimize-tsconfig/tsconfig.json | 3 +- .../scripts/optimize-tsconfig/unoptimize.js | 0 .../plugins/apm/scripts/package.json | 0 .../apm/scripts/setup-kibana-security.js | 0 .../plugins/apm/scripts/storybook.js | 0 .../apm/scripts/unoptimize-tsconfig.js | 0 .../apm/scripts/upload-telemetry-data.js | 0 .../download-telemetry-template.ts | 0 .../generate-sample-documents.ts | 2 +- .../scripts/upload-telemetry-data/index.ts | 2 +- x-pack/plugins/apm/server/feature.ts | 72 ++ x-pack/plugins/apm/server/index.ts | 2 +- .../lib/errors/distribution/queries.test.ts | 2 +- .../apm/server/lib/errors/queries.test.ts | 2 +- .../apm/server/lib/helpers/es_client.ts | 2 +- .../server/lib/helpers/setup_request.test.ts | 13 - .../get_apm_index_pattern_title.ts | 10 + .../apm/server/lib/metrics/queries.test.ts | 2 +- .../server/lib/service_nodes/queries.test.ts | 2 +- .../lib/services/annotations/index.test.ts | 2 +- .../apm/server/lib/services/queries.test.ts | 2 +- .../agent_configuration/queries.test.ts | 2 +- .../create_or_update_custom_link.test.ts | 2 +- .../custom_link/get_transaction.test.ts | 2 +- .../custom_link/list_custom_links.test.ts | 2 +- .../apm/server/lib/traces/queries.test.ts | 2 +- .../lib/transaction_groups/queries.test.ts | 2 +- .../server/lib/transactions/queries.test.ts | 2 +- .../local_ui_filters/queries.test.ts | 2 +- .../apm/server/lib/ui_filters/queries.test.ts | 2 +- x-pack/plugins/apm/server/plugin.ts | 40 +- .../server/routes/create_api/index.test.ts | 4 +- .../apm/server/routes/create_api/index.ts | 3 +- .../apm/server/routes/create_apm_api.ts | 4 +- .../apm/server/routes/index_pattern.ts | 8 + x-pack/plugins/apm/server/routes/typings.ts | 7 +- .../apm/server/saved_objects/apm_indices.ts | 34 + .../apm/server/saved_objects/apm_telemetry.ts | 921 +++++++++++++++++ .../plugins/apm/server/saved_objects/index.ts | 8 + x-pack/plugins/apm/typings/common.d.ts | 8 +- .../log_entry_actions_menu.tsx | 2 +- .../lib/adapters/framework/adapter_types.ts | 4 +- .../siem/scripts/optimize_tsconfig/README.md | 2 +- .../calculate_timeseries_interval.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../kuery_bar/typeahead/suggestion.js | 3 +- .../kuery_bar/typeahead/suggestions.js | 3 +- x-pack/tsconfig.json | 2 +- 560 files changed, 1748 insertions(+), 1902 deletions(-) delete mode 100644 x-pack/legacy/plugins/apm/.prettierrc delete mode 100644 x-pack/legacy/plugins/apm/e2e/cypress/integration/snapshots.js delete mode 100644 x-pack/legacy/plugins/apm/index.ts delete mode 100644 x-pack/legacy/plugins/apm/mappings.json delete mode 100644 x-pack/legacy/plugins/apm/public/index.scss delete mode 100644 x-pack/legacy/plugins/apm/public/index.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/legacy_register_feature.ts delete mode 100644 x-pack/legacy/plugins/apm/public/new-platform/getConfigFromInjectedMetadata.ts delete mode 100644 x-pack/legacy/plugins/apm/public/new-platform/index.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/style/global_overrides.css delete mode 100644 x-pack/legacy/plugins/apm/public/templates/index.html rename x-pack/{legacy => }/plugins/apm/CONTRIBUTING.md (100%) rename x-pack/{legacy/plugins/apm/public/utils/pickKeys.ts => plugins/apm/common/utils/pick_keys.ts} (100%) rename x-pack/{legacy => }/plugins/apm/dev_docs/github_commands.md (100%) rename x-pack/{legacy => }/plugins/apm/dev_docs/typescript.md (83%) rename x-pack/{legacy => }/plugins/apm/dev_docs/vscode_setup.md (83%) rename x-pack/{legacy => }/plugins/apm/e2e/.gitignore (100%) rename x-pack/{legacy => }/plugins/apm/e2e/README.md (84%) rename x-pack/{legacy => }/plugins/apm/e2e/ci/Dockerfile (100%) rename x-pack/{legacy => }/plugins/apm/e2e/ci/entrypoint.sh (90%) rename x-pack/{legacy => }/plugins/apm/e2e/ci/kibana.e2e.yml (100%) rename x-pack/{legacy => }/plugins/apm/e2e/ci/prepare-kibana.sh (95%) rename x-pack/{legacy => }/plugins/apm/e2e/cypress.json (100%) rename x-pack/{legacy => }/plugins/apm/e2e/cypress/fixtures/example.json (100%) rename x-pack/{legacy => }/plugins/apm/e2e/cypress/integration/apm.feature (100%) rename x-pack/{legacy => }/plugins/apm/e2e/cypress/integration/helpers.ts (100%) create mode 100644 x-pack/plugins/apm/e2e/cypress/integration/snapshots.js rename x-pack/{legacy => }/plugins/apm/e2e/cypress/plugins/index.js (100%) rename x-pack/{legacy => }/plugins/apm/e2e/cypress/support/commands.js (100%) rename x-pack/{legacy => }/plugins/apm/e2e/cypress/support/index.ts (100%) rename x-pack/{legacy => }/plugins/apm/e2e/cypress/support/step_definitions/apm.ts (100%) rename x-pack/{legacy => }/plugins/apm/e2e/cypress/typings/index.d.ts (100%) rename x-pack/{legacy => }/plugins/apm/e2e/cypress/webpack.config.js (100%) rename x-pack/{legacy => }/plugins/apm/e2e/ingest-data/replay.js (100%) rename x-pack/{legacy => }/plugins/apm/e2e/package.json (100%) rename x-pack/{legacy => }/plugins/apm/e2e/run-e2e.sh (98%) rename x-pack/{legacy => }/plugins/apm/e2e/tsconfig.json (100%) rename x-pack/{legacy => }/plugins/apm/e2e/yarn.lock (100%) create mode 100644 x-pack/plugins/apm/public/application/index.tsx rename x-pack/{legacy => }/plugins/apm/public/components/app/APMIndicesPermission/__test__/APMIndicesPermission.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/APMIndicesPermission/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx (92%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx (92%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Home/Home.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Home/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Main/ScrollToTopOnPathChange.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Main/__test__/ProvideBreadcrumbs.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Main/route_config/index.tsx (98%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Main/route_config/route_names.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx (82%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts (98%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceDetails/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/Controls.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx (98%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts (98%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons.ts (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/aws.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/dark.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/database.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/default.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/documents.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/globe.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/go.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/java.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/php.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/python.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/redis.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/index.test.tsx (94%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/index.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceMetrics/index.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx (98%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx (94%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/ServiceOverview/index.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx (92%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx (94%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts (90%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx (98%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx (94%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts (91%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx (99%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/Settings/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TraceLink/index.tsx (92%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TraceOverview/TraceList.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TraceOverview/index.tsx (91%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts (92%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts (92%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts (87%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts (89%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx (90%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx (92%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx (86%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts (98%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionDetails/index.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionOverview/List/index.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionOverview/index.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/app/TransactionOverview/useRedirect.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/ApmHeader/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/DatePicker/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/EmptyMessage.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx (92%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/ErrorStatePrompt.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/EuiTabLink.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/HeightRetainer/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/ImpactBar/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/KeyValueTable/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js (95%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts (92%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/KueryBar/index.tsx (98%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/LicensePrompt/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx (85%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx (79%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx (85%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx (94%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx (94%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx (92%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx (90%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx (88%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionButton.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mockTransaction.json (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/InfraLink.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/InfraLink.tsx (94%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/KibanaLink.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx (88%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/APMLink.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx (92%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx (92%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx (94%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx (87%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/rison_helpers.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/url_helpers.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Links/url_helpers.ts (87%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/LoadingStatePrompt.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/LocalUIFilters/index.tsx (91%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/ManagedTable/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx (87%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/Section.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx (88%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx (87%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts (96%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/helper.ts (85%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/MetadataTable/sections.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/Context.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/Variables.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/index.test.ts.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts (97%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/__test__/stacktraces.json (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Stacktrace/index.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js (95%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/StickyProperties/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/HttpStatusBadge/statusCodes.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/TransactionResultSummaryItem.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx (91%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx (90%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts (92%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/Summary/index.tsx (94%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TimestampTooltip/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx (91%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx (90%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx (85%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx (86%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx (94%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx (92%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx (98%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts (98%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts (98%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx (81%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx (95%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx (92%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/SelectionMarker.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/StatusText.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/getEmptySeries.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/index.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts (94%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Histogram/index.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Legend/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx (90%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/LastTickValue.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx (97%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/AgentMarker.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/ErrorMarker.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/Marker.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/AgentMarker.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/ErrorMarker.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/Marker.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Timeline/plotUtils.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/Tooltip/index.js (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx (96%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/charts/helper/timezone.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/components/shared/useDelayedVisibility/index.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx (93%) rename x-pack/{legacy => }/plugins/apm/public/context/ApmPluginContext/index.tsx (87%) rename x-pack/{legacy => }/plugins/apm/public/context/ChartsSyncContext.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/context/LicenseContext/index.tsx (94%) rename x-pack/{legacy => }/plugins/apm/public/context/LoadingIndicatorContext.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/context/LocationContext.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/context/MatchedRouteContext.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/context/UrlParamsContext/constants.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/context/UrlParamsContext/helpers.ts (97%) rename x-pack/{legacy => }/plugins/apm/public/context/UrlParamsContext/index.tsx (92%) rename x-pack/{legacy => }/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts (93%) rename x-pack/{legacy => }/plugins/apm/public/context/UrlParamsContext/types.ts (83%) rename x-pack/{legacy/plugins/apm/public/new-platform => plugins/apm/public}/featureCatalogueEntry.ts (88%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useAgentName.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useApmPluginContext.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useAvgDurationByBrowser.ts (83%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useAvgDurationByCountry.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useCallApi.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useChartsSync.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useComponentId.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useDeepObjectIdentity.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useDynamicIndexPattern.ts (89%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useFetcher.integration.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useFetcher.test.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useFetcher.tsx (98%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useKibanaUrl.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useLicense.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useLoadingIndicator.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useLocalUIFilters.ts (88%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useLocation.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useMatchedRoutes.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useServiceMetricCharts.ts (91%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useServiceTransactionTypes.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useTransactionBreakdown.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useTransactionCharts.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useTransactionDistribution.ts (93%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useTransactionList.ts (94%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useUrlParams.tsx (100%) rename x-pack/{legacy => }/plugins/apm/public/hooks/useWaterfall.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/icon.svg (100%) rename x-pack/{legacy => }/plugins/apm/public/images/apm-ml-anomaly-detection-example.png (100%) create mode 100644 x-pack/plugins/apm/public/index.ts create mode 100644 x-pack/plugins/apm/public/plugin.ts rename x-pack/{legacy => }/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/selectors/chartSelectors.ts (94%) rename x-pack/{legacy => }/plugins/apm/public/services/__test__/SessionStorageMock.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/services/__test__/callApi.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/services/__test__/callApmApi.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/services/rest/callApi.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/services/rest/createCallApmApi.ts (89%) rename x-pack/{legacy => }/plugins/apm/public/services/rest/index_pattern.ts (76%) rename x-pack/{legacy => }/plugins/apm/public/services/rest/ml.ts (91%) rename x-pack/{legacy => }/plugins/apm/public/services/rest/watcher.ts (100%) rename x-pack/{legacy/plugins/apm/public/new-platform => plugins/apm/public}/setHelpExtension.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/style/variables.ts (100%) rename x-pack/{legacy/plugins/apm/public/new-platform => plugins/apm/public}/toggleAppLinkInNav.ts (91%) rename x-pack/{legacy/plugins/apm/public/new-platform => plugins/apm/public}/updateBadge.ts (99%) rename x-pack/{legacy => }/plugins/apm/public/utils/__test__/flattenObject.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/utils/flattenObject.ts (94%) rename x-pack/{legacy => }/plugins/apm/public/utils/formatters/__test__/datetime.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/utils/formatters/__test__/duration.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/utils/formatters/__test__/formatters.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/utils/formatters/__test__/size.test.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/utils/formatters/datetime.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/utils/formatters/duration.ts (96%) rename x-pack/{legacy => }/plugins/apm/public/utils/formatters/formatters.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/utils/formatters/index.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/utils/formatters/size.ts (95%) rename x-pack/{legacy => }/plugins/apm/public/utils/getRangeFromTimeSeries.ts (88%) rename x-pack/{legacy => }/plugins/apm/public/utils/history.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/utils/httpStatusCodeToColor.ts (100%) rename x-pack/{legacy => }/plugins/apm/public/utils/isValidCoordinateValue.ts (84%) rename x-pack/{legacy => }/plugins/apm/public/utils/testHelpers.tsx (96%) rename x-pack/{legacy => }/plugins/apm/readme.md (92%) rename x-pack/{legacy => }/plugins/apm/scripts/.gitignore (100%) rename x-pack/{legacy => }/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts (100%) rename x-pack/{legacy => }/plugins/apm/scripts/optimize-tsconfig.js (100%) rename x-pack/{legacy => }/plugins/apm/scripts/optimize-tsconfig/optimize.js (100%) rename x-pack/{legacy => }/plugins/apm/scripts/optimize-tsconfig/paths.js (100%) rename x-pack/{legacy => }/plugins/apm/scripts/optimize-tsconfig/tsconfig.json (60%) rename x-pack/{legacy => }/plugins/apm/scripts/optimize-tsconfig/unoptimize.js (100%) rename x-pack/{legacy => }/plugins/apm/scripts/package.json (100%) rename x-pack/{legacy => }/plugins/apm/scripts/setup-kibana-security.js (100%) rename x-pack/{legacy => }/plugins/apm/scripts/storybook.js (100%) rename x-pack/{legacy => }/plugins/apm/scripts/unoptimize-tsconfig.js (100%) rename x-pack/{legacy => }/plugins/apm/scripts/upload-telemetry-data.js (100%) rename x-pack/{legacy => }/plugins/apm/scripts/upload-telemetry-data/download-telemetry-template.ts (100%) rename x-pack/{legacy => }/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts (97%) rename x-pack/{legacy => }/plugins/apm/scripts/upload-telemetry-data/index.ts (98%) create mode 100644 x-pack/plugins/apm/server/feature.ts create mode 100644 x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts create mode 100644 x-pack/plugins/apm/server/saved_objects/apm_indices.ts create mode 100644 x-pack/plugins/apm/server/saved_objects/apm_telemetry.ts create mode 100644 x-pack/plugins/apm/server/saved_objects/index.ts diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index f16ba61234a95..97dec3eead303 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -266,8 +266,8 @@ export class ClusterManager { fromRoot('src/legacy/server/sass/__tmp__'), fromRoot('x-pack/legacy/plugins/reporting/.chromium'), fromRoot('x-pack/plugins/siem/cypress'), - fromRoot('x-pack/legacy/plugins/apm/e2e'), - fromRoot('x-pack/legacy/plugins/apm/scripts'), + fromRoot('x-pack/plugins/apm/e2e'), + fromRoot('x-pack/plugins/apm/scripts'), fromRoot('x-pack/legacy/plugins/canvas/canvas_plugin_src'), // prevents server from restarting twice for Canvas plugin changes, 'plugins/java_languageserver', ]; diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 14e25ab863dbc..8630221b3e94f 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -35,7 +35,6 @@ export const IGNORE_FILE_GLOBS = [ '**/Gruntfile.js', 'tasks/config/**/*', '**/{Dockerfile,docker-compose.yml}', - 'x-pack/legacy/plugins/apm/**/*', 'x-pack/legacy/plugins/canvas/tasks/**/*', 'x-pack/legacy/plugins/canvas/canvas_plugin_src/**/*', 'x-pack/plugins/monitoring/public/lib/jquery_flot/**/*', @@ -59,6 +58,11 @@ export const IGNORE_FILE_GLOBS = [ // filename required by api-extractor 'api-documenter.json', + + // TODO fix file names in APM to remove these + 'x-pack/plugins/apm/public/**/*', + 'x-pack/plugins/apm/scripts/**/*', + 'x-pack/plugins/apm/e2e/**/*', ]; /** diff --git a/src/dev/run_check_lockfile_symlinks.js b/src/dev/run_check_lockfile_symlinks.js index 6c6fc54638ee8..b912ea9ddb87e 100644 --- a/src/dev/run_check_lockfile_symlinks.js +++ b/src/dev/run_check_lockfile_symlinks.js @@ -35,9 +35,9 @@ const IGNORE_FILE_GLOBS = [ // fixtures aren't used in production, ignore them '**/*fixtures*/**/*', // cypress isn't used in production, ignore it - 'x-pack/legacy/plugins/apm/e2e/*', + 'x-pack/plugins/apm/e2e/*', // apm scripts aren't used in production, ignore them - 'x-pack/legacy/plugins/apm/scripts/*', + 'x-pack/plugins/apm/scripts/*', ]; run(async ({ log }) => { diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 43114b2edccfc..4dc930dae3e25 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -18,7 +18,7 @@ */ export const storybookAliases = { - apm: 'x-pack/legacy/plugins/apm/scripts/storybook.js', + apm: 'x-pack/plugins/apm/scripts/storybook.js', canvas: 'x-pack/legacy/plugins/canvas/scripts/storybook_new.js', codeeditor: 'src/plugins/kibana_react/public/code_editor/scripts/storybook.ts', drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js', diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 01d8a30b598c1..a13f61af60173 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -30,7 +30,7 @@ export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'x-pack/plugins/siem/cypress/tsconfig.json'), { name: 'siem/cypress', }), - new Project(resolve(REPO_ROOT, 'x-pack/legacy/plugins/apm/e2e/tsconfig.json'), { + new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/e2e/tsconfig.json'), { name: 'apm/cypress', disableTypeCheck: true, }), diff --git a/x-pack/index.js b/x-pack/index.js index cfadddac3994a..89cbb03f084eb 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -10,7 +10,6 @@ import { reporting } from './legacy/plugins/reporting'; import { security } from './legacy/plugins/security'; import { dashboardMode } from './legacy/plugins/dashboard_mode'; import { beats } from './legacy/plugins/beats_management'; -import { apm } from './legacy/plugins/apm'; import { maps } from './legacy/plugins/maps'; import { spaces } from './legacy/plugins/spaces'; import { canvas } from './legacy/plugins/canvas'; @@ -28,7 +27,6 @@ module.exports = function(kibana) { security(kibana), dashboardMode(kibana), beats(kibana), - apm(kibana), maps(kibana), canvas(kibana), infra(kibana), diff --git a/x-pack/legacy/plugins/apm/.prettierrc b/x-pack/legacy/plugins/apm/.prettierrc deleted file mode 100644 index 650cb880f6f5a..0000000000000 --- a/x-pack/legacy/plugins/apm/.prettierrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "singleQuote": true, - "semi": true -} diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/legacy/plugins/apm/e2e/cypress/integration/snapshots.js deleted file mode 100644 index 968c2675a62e7..0000000000000 --- a/x-pack/legacy/plugins/apm/e2e/cypress/integration/snapshots.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - "APM": { - "Transaction duration charts": { - "1": "500 ms", - "2": "250 ms", - "3": "0 ms" - } - }, - "__version": "4.2.0" -} diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts deleted file mode 100644 index d2383acd45eba..0000000000000 --- a/x-pack/legacy/plugins/apm/index.ts +++ /dev/null @@ -1,166 +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 { i18n } from '@kbn/i18n'; -import { Server } from 'hapi'; -import { resolve } from 'path'; -import { APMPluginContract } from '../../../plugins/apm/server'; -import { LegacyPluginInitializer } from '../../../../src/legacy/types'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; -import mappings from './mappings.json'; - -export const apm: LegacyPluginInitializer = kibana => { - return new kibana.Plugin({ - require: [ - 'kibana', - 'elasticsearch', - 'xpack_main', - 'apm_oss', - 'task_manager' - ], - id: 'apm', - configPrefix: 'xpack.apm', - publicDir: resolve(__dirname, 'public'), - uiExports: { - app: { - title: 'APM', - description: i18n.translate('xpack.apm.apmForESDescription', { - defaultMessage: 'APM for the Elastic Stack' - }), - main: 'plugins/apm/index', - icon: 'plugins/apm/icon.svg', - euiIconType: 'apmApp', - order: 8100, - category: DEFAULT_APP_CATEGORIES.observability - }, - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - home: ['plugins/apm/legacy_register_feature'], - - // TODO: get proper types - injectDefaultVars(server: Server) { - const config = server.config(); - return { - apmUiEnabled: config.get('xpack.apm.ui.enabled'), - // TODO: rename to apm_oss.indexPatternTitle in 7.0 (breaking change) - apmIndexPatternTitle: config.get('apm_oss.indexPattern'), - apmServiceMapEnabled: config.get('xpack.apm.serviceMapEnabled') - }; - }, - savedObjectSchemas: { - 'apm-services-telemetry': { - isNamespaceAgnostic: true - }, - 'apm-indices': { - isNamespaceAgnostic: true - } - }, - mappings - }, - - // TODO: get proper types - config(Joi: any) { - return Joi.object({ - // display menu item - ui: Joi.object({ - enabled: Joi.boolean().default(true), - transactionGroupBucketSize: Joi.number().default(100), - maxTraceItems: Joi.number().default(1000) - }).default(), - - // enable plugin - enabled: Joi.boolean().default(true), - - // index patterns - autocreateApmIndexPattern: Joi.boolean().default(true), - - // service map - serviceMapEnabled: Joi.boolean().default(true), - serviceMapFingerprintBucketSize: Joi.number().default(100), - serviceMapTraceIdBucketSize: Joi.number().default(65), - serviceMapFingerprintGlobalBucketSize: Joi.number().default(1000), - serviceMapTraceIdGlobalBucketSize: Joi.number().default(6), - serviceMapMaxTracesPerRequest: Joi.number().default(50), - - // telemetry - telemetryCollectionEnabled: Joi.boolean().default(true) - }).default(); - }, - - // TODO: get proper types - init(server: Server) { - server.plugins.xpack_main.registerFeature({ - id: 'apm', - name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { - defaultMessage: 'APM' - }), - order: 900, - icon: 'apmApp', - navLinkId: 'apm', - app: ['apm', 'kibana'], - catalogue: ['apm'], - // see x-pack/plugins/features/common/feature_kibana_privileges.ts - privileges: { - all: { - app: ['apm', 'kibana'], - api: [ - 'apm', - 'apm_write', - 'actions-read', - 'actions-all', - 'alerting-read', - 'alerting-all' - ], - catalogue: ['apm'], - savedObject: { - all: ['alert', 'action', 'action_task_params'], - read: [] - }, - ui: [ - 'show', - 'save', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete' - ] - }, - read: { - app: ['apm', 'kibana'], - api: [ - 'apm', - 'actions-read', - 'actions-all', - 'alerting-read', - 'alerting-all' - ], - catalogue: ['apm'], - savedObject: { - all: ['alert', 'action', 'action_task_params'], - read: [] - }, - ui: [ - 'show', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete' - ] - } - } - }); - const apmPlugin = server.newPlatform.setup.plugins - .apm as APMPluginContract; - - apmPlugin.registerLegacyAPI({ - server - }); - } - }); -}; diff --git a/x-pack/legacy/plugins/apm/mappings.json b/x-pack/legacy/plugins/apm/mappings.json deleted file mode 100644 index 6ca9f13792085..0000000000000 --- a/x-pack/legacy/plugins/apm/mappings.json +++ /dev/null @@ -1,933 +0,0 @@ -{ - "apm-telemetry": { - "properties": { - "agents": { - "properties": { - "dotnet": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "go": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "java": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "js-base": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "nodejs": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "python": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "ruby": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "rum-js": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - } - } - }, - "counts": { - "properties": { - "agent_configuration": { - "properties": { - "all": { - "type": "long" - } - } - }, - "error": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "max_error_groups_per_service": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "max_transaction_groups_per_service": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "metric": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "onboarding": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "services": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "sourcemap": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "span": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "traces": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "transaction": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - } - } - }, - "cardinality": { - "properties": { - "user_agent": { - "properties": { - "original": { - "properties": { - "all_agents": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "rum": { - "properties": { - "1d": { - "type": "long" - } - } - } - } - } - } - }, - "transaction": { - "properties": { - "name": { - "properties": { - "all_agents": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "rum": { - "properties": { - "1d": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "has_any_services": { - "type": "boolean" - }, - "indices": { - "properties": { - "all": { - "properties": { - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - } - } - } - } - }, - "shards": { - "properties": { - "total": { - "type": "long" - } - } - } - } - }, - "integrations": { - "properties": { - "ml": { - "properties": { - "all_jobs_count": { - "type": "long" - } - } - } - } - }, - "retainment": { - "properties": { - "error": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "metric": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "onboarding": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "span": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "transaction": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "services_per_agent": { - "properties": { - "dotnet": { - "type": "long", - "null_value": 0 - }, - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - }, - "rum-js": { - "type": "long", - "null_value": 0 - } - } - }, - "tasks": { - "properties": { - "agent_configuration": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "agents": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "cardinality": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "groupings": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "indices_stats": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "integrations": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "processor_events": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "services": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "versions": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - } - } - }, - "version": { - "properties": { - "apm_server": { - "properties": { - "major": { - "type": "long" - }, - "minor": { - "type": "long" - }, - "patch": { - "type": "long" - } - } - } - } - } - } - }, - "apm-indices": { - "properties": { - "apm_oss.sourcemapIndices": { - "type": "keyword" - }, - "apm_oss.errorIndices": { - "type": "keyword" - }, - "apm_oss.onboardingIndices": { - "type": "keyword" - }, - "apm_oss.spanIndices": { - "type": "keyword" - }, - "apm_oss.transactionIndices": { - "type": "keyword" - }, - "apm_oss.metricsIndices": { - "type": "keyword" - } - } - } -} diff --git a/x-pack/legacy/plugins/apm/public/index.scss b/x-pack/legacy/plugins/apm/public/index.scss deleted file mode 100644 index 04a070c304d6f..0000000000000 --- a/x-pack/legacy/plugins/apm/public/index.scss +++ /dev/null @@ -1,16 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -/* APM plugin styles */ - -// Prefix all styles with "apm" to avoid conflicts. -// Examples -// apmChart -// apmChart__legend -// apmChart__legend--small -// apmChart__legend-isLoading - -.apmReactRoot { - overflow-x: auto; - height: 100%; -} diff --git a/x-pack/legacy/plugins/apm/public/index.tsx b/x-pack/legacy/plugins/apm/public/index.tsx deleted file mode 100644 index 59b2fedaafba6..0000000000000 --- a/x-pack/legacy/plugins/apm/public/index.tsx +++ /dev/null @@ -1,36 +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 { npSetup, npStart } from 'ui/new_platform'; -import 'react-vis/dist/style.css'; -import { PluginInitializerContext } from 'kibana/public'; -import 'ui/autoload/all'; -import chrome from 'ui/chrome'; -import { plugin } from './new-platform'; -import { REACT_APP_ROOT_ID } from './new-platform/plugin'; -import './style/global_overrides.css'; -import template from './templates/index.html'; - -// This will be moved to core.application.register when the new platform -// migration is complete. -// @ts-ignore -chrome.setRootTemplate(template); - -const checkForRoot = () => { - return new Promise(resolve => { - const ready = !!document.getElementById(REACT_APP_ROOT_ID); - if (ready) { - resolve(); - } else { - setTimeout(() => resolve(checkForRoot()), 10); - } - }); -}; -checkForRoot().then(() => { - const pluginInstance = plugin({} as PluginInitializerContext); - pluginInstance.setup(npSetup.core, npSetup.plugins); - pluginInstance.start(npStart.core, npStart.plugins); -}); diff --git a/x-pack/legacy/plugins/apm/public/legacy_register_feature.ts b/x-pack/legacy/plugins/apm/public/legacy_register_feature.ts deleted file mode 100644 index f12865399054e..0000000000000 --- a/x-pack/legacy/plugins/apm/public/legacy_register_feature.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 { npSetup } from 'ui/new_platform'; -import { featureCatalogueEntry } from './new-platform/featureCatalogueEntry'; - -const { - core, - plugins: { home } -} = npSetup; -const apmUiEnabled = core.injectedMetadata.getInjectedVar( - 'apmUiEnabled' -) as boolean; - -if (apmUiEnabled) { - home.featureCatalogue.register(featureCatalogueEntry); -} - -home.environment.update({ - apmUi: apmUiEnabled -}); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/getConfigFromInjectedMetadata.ts b/x-pack/legacy/plugins/apm/public/new-platform/getConfigFromInjectedMetadata.ts deleted file mode 100644 index 6dc77f7733b2d..0000000000000 --- a/x-pack/legacy/plugins/apm/public/new-platform/getConfigFromInjectedMetadata.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 { npStart } from 'ui/new_platform'; -import { ConfigSchema } from './plugin'; - -const { core } = npStart; - -export function getConfigFromInjectedMetadata(): ConfigSchema { - const { - apmIndexPatternTitle, - apmServiceMapEnabled, - apmUiEnabled - } = core.injectedMetadata.getInjectedVars(); - - return { - indexPatternTitle: `${apmIndexPatternTitle}`, - serviceMapEnabled: !!apmServiceMapEnabled, - ui: { enabled: !!apmUiEnabled } - }; -} diff --git a/x-pack/legacy/plugins/apm/public/new-platform/index.tsx b/x-pack/legacy/plugins/apm/public/new-platform/index.tsx deleted file mode 100644 index 0674dc48316f4..0000000000000 --- a/x-pack/legacy/plugins/apm/public/new-platform/index.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 { PluginInitializer } from '../../../../../../src/core/public'; -import { ApmPlugin, ApmPluginSetup, ApmPluginStart } from './plugin'; - -export const plugin: PluginInitializer< - ApmPluginSetup, - ApmPluginStart -> = pluginInitializerContext => new ApmPlugin(pluginInitializerContext); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx deleted file mode 100644 index 80a45ba66c4fa..0000000000000 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ /dev/null @@ -1,203 +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 { ApmRoute } from '@elastic/apm-rum-react'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Route, Router, Switch } from 'react-router-dom'; -import styled from 'styled-components'; -import { - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext -} from '../../../../../../src/core/public'; -import { DataPublicPluginSetup } from '../../../../../../src/plugins/data/public'; -import { HomePublicPluginSetup } from '../../../../../../src/plugins/home/public'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { PluginSetupContract as AlertingPluginPublicSetup } from '../../../../../plugins/alerting/public'; -import { AlertType } from '../../../../../plugins/apm/common/alert_types'; -import { LicensingPluginSetup } from '../../../../../plugins/licensing/public'; -import { - AlertsContextProvider, - TriggersAndActionsUIPublicPluginSetup -} from '../../../../../plugins/triggers_actions_ui/public'; -import { APMIndicesPermission } from '../components/app/APMIndicesPermission'; -import { routes } from '../components/app/Main/route_config'; -import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; -import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; -import { ErrorRateAlertTrigger } from '../components/shared/ErrorRateAlertTrigger'; -import { TransactionDurationAlertTrigger } from '../components/shared/TransactionDurationAlertTrigger'; -import { ApmPluginContext } from '../context/ApmPluginContext'; -import { LicenseProvider } from '../context/LicenseContext'; -import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext'; -import { LocationProvider } from '../context/LocationContext'; -import { MatchedRouteProvider } from '../context/MatchedRouteContext'; -import { UrlParamsProvider } from '../context/UrlParamsContext'; -import { createCallApmApi } from '../services/rest/createCallApmApi'; -import { createStaticIndexPattern } from '../services/rest/index_pattern'; -import { px, unit, units } from '../style/variables'; -import { history } from '../utils/history'; -import { featureCatalogueEntry } from './featureCatalogueEntry'; -import { getConfigFromInjectedMetadata } from './getConfigFromInjectedMetadata'; -import { setHelpExtension } from './setHelpExtension'; -import { toggleAppLinkInNav } from './toggleAppLinkInNav'; -import { setReadonlyBadge } from './updateBadge'; - -export const REACT_APP_ROOT_ID = 'react-apm-root'; - -const MainContainer = styled.div` - min-width: ${px(unit * 50)}; - padding: ${px(units.plus)}; - height: 100%; -`; - -const App = () => { - return ( - - - - - - {routes.map((route, i) => ( - - ))} - - - - ); -}; - -export type ApmPluginSetup = void; -export type ApmPluginStart = void; - -export interface ApmPluginSetupDeps { - alerting?: AlertingPluginPublicSetup; - data: DataPublicPluginSetup; - home: HomePublicPluginSetup; - licensing: LicensingPluginSetup; - triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; -} - -export interface ConfigSchema { - indexPatternTitle: string; - serviceMapEnabled: boolean; - ui: { - enabled: boolean; - }; -} - -export class ApmPlugin - implements Plugin { - // When we switch over from the old platform to new platform the plugins will - // be coming from setup instead of start, since that's where we do - // `core.application.register`. During the transitions we put plugins on an - // instance property so we can use it in start. - setupPlugins: ApmPluginSetupDeps = {} as ApmPluginSetupDeps; - - constructor( - // @ts-ignore Not using initializerContext now, but will be once NP - // migration is complete. - private readonly initializerContext: PluginInitializerContext - ) {} - - // Take the DOM element as the constructor, so we can mount the app. - public setup(_core: CoreSetup, plugins: ApmPluginSetupDeps) { - plugins.home.featureCatalogue.register(featureCatalogueEntry); - this.setupPlugins = plugins; - } - - public start(core: CoreStart) { - const i18nCore = core.i18n; - const plugins = this.setupPlugins; - createCallApmApi(core.http); - - // Once we're actually an NP plugin we'll get the config from the - // initializerContext like: - // - // const config = this.initializerContext.config.get(); - // - // Until then we use a shim to get it from legacy injectedMetadata: - const config = getConfigFromInjectedMetadata(); - - // render APM feedback link in global help menu - setHelpExtension(core); - setReadonlyBadge(core); - toggleAppLinkInNav(core, config); - - const apmPluginContextValue = { - config, - core, - plugins - }; - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.ErrorRate, - name: i18n.translate('xpack.apm.alertTypes.errorRate', { - defaultMessage: 'Error rate' - }), - iconClass: 'bell', - alertParamsExpression: ErrorRateAlertTrigger, - validate: () => ({ - errors: [] - }) - }); - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.TransactionDuration, - name: i18n.translate('xpack.apm.alertTypes.transactionDuration', { - defaultMessage: 'Transaction duration' - }), - iconClass: 'bell', - alertParamsExpression: TransactionDurationAlertTrigger, - validate: () => ({ - errors: [] - }) - }); - - ReactDOM.render( - - - - - - - - - - - - - - - - - - - - - , - document.getElementById(REACT_APP_ROOT_ID) - ); - - // create static index pattern and store as saved object. Not needed by APM UI but for legacy reasons in Discover, Dashboard etc. - createStaticIndexPattern().catch(e => { - // eslint-disable-next-line no-console - console.log('Error fetching static index pattern', e); - }); - } - - public stop() {} -} diff --git a/x-pack/legacy/plugins/apm/public/style/global_overrides.css b/x-pack/legacy/plugins/apm/public/style/global_overrides.css deleted file mode 100644 index 75b4532f7c9a1..0000000000000 --- a/x-pack/legacy/plugins/apm/public/style/global_overrides.css +++ /dev/null @@ -1,34 +0,0 @@ -/* -Hide unused secondary Kibana navigation -*/ -.kuiLocalNav { - min-height: initial; -} - -.kuiLocalNavRow.kuiLocalNavRow--secondary { - display: none; -} - -/* -Remove unnecessary space below the navigation dropdown -*/ -.kuiLocalDropdown { - margin-bottom: 0; - border-bottom: none; -} - -/* -Hide the "0-10 of 100" text in KUIPager component for all KUIControlledTable -*/ -.kuiControlledTable .kuiPagerText { - display: none; -} - -/* -Hide default dashed gridlines in EUI chart component for all APM graphs -*/ - -.rv-xy-plot__grid-lines__line { - stroke-opacity: 1; - stroke-dasharray: 1; -} diff --git a/x-pack/legacy/plugins/apm/public/templates/index.html b/x-pack/legacy/plugins/apm/public/templates/index.html deleted file mode 100644 index 78e0ade3ad624..0000000000000 --- a/x-pack/legacy/plugins/apm/public/templates/index.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/x-pack/legacy/plugins/apm/CONTRIBUTING.md b/x-pack/plugins/apm/CONTRIBUTING.md similarity index 100% rename from x-pack/legacy/plugins/apm/CONTRIBUTING.md rename to x-pack/plugins/apm/CONTRIBUTING.md diff --git a/x-pack/plugins/apm/common/apm_saved_object_constants.ts b/x-pack/plugins/apm/common/apm_saved_object_constants.ts index 0529d90fe940a..eb16db7715fed 100644 --- a/x-pack/plugins/apm/common/apm_saved_object_constants.ts +++ b/x-pack/plugins/apm/common/apm_saved_object_constants.ts @@ -5,7 +5,7 @@ */ // the types have to match the names of the saved object mappings -// in /x-pack/legacy/plugins/apm/mappings.json +// in /x-pack/plugins/apm/mappings.json // APM indices export const APM_INDICES_SAVED_OBJECT_TYPE = 'apm-indices'; diff --git a/x-pack/legacy/plugins/apm/public/utils/pickKeys.ts b/x-pack/plugins/apm/common/utils/pick_keys.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/pickKeys.ts rename to x-pack/plugins/apm/common/utils/pick_keys.ts diff --git a/x-pack/legacy/plugins/apm/dev_docs/github_commands.md b/x-pack/plugins/apm/dev_docs/github_commands.md similarity index 100% rename from x-pack/legacy/plugins/apm/dev_docs/github_commands.md rename to x-pack/plugins/apm/dev_docs/github_commands.md diff --git a/x-pack/legacy/plugins/apm/dev_docs/typescript.md b/x-pack/plugins/apm/dev_docs/typescript.md similarity index 83% rename from x-pack/legacy/plugins/apm/dev_docs/typescript.md rename to x-pack/plugins/apm/dev_docs/typescript.md index 6858e93ec09e0..6de61b665a1b1 100644 --- a/x-pack/legacy/plugins/apm/dev_docs/typescript.md +++ b/x-pack/plugins/apm/dev_docs/typescript.md @@ -4,8 +4,8 @@ Kibana and X-Pack are very large TypeScript projects, and it comes at a cost. Ed To run the optimization: -`$ node x-pack/legacy/plugins/apm/scripts/optimize-tsconfig` +`$ node x-pack/plugins/apm/scripts/optimize-tsconfig` To undo the optimization: -`$ node x-pack/legacy/plugins/apm/scripts/unoptimize-tsconfig` +`$ node x-pack/plugins/apm/scripts/unoptimize-tsconfig` diff --git a/x-pack/legacy/plugins/apm/dev_docs/vscode_setup.md b/x-pack/plugins/apm/dev_docs/vscode_setup.md similarity index 83% rename from x-pack/legacy/plugins/apm/dev_docs/vscode_setup.md rename to x-pack/plugins/apm/dev_docs/vscode_setup.md index e1901b3855f73..1c80d1476520d 100644 --- a/x-pack/legacy/plugins/apm/dev_docs/vscode_setup.md +++ b/x-pack/plugins/apm/dev_docs/vscode_setup.md @@ -1,6 +1,6 @@ ### Visual Studio Code -When using [Visual Studio Code](https://code.visualstudio.com/) with APM it's best to set up a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) and add the `x-pack/legacy/plugins/apm` directory, the `x-pack` directory, and the root of the Kibana repository to the workspace. This makes it so you can navigate and search within APM and use the wider workspace roots when you need to widen your search. +When using [Visual Studio Code](https://code.visualstudio.com/) with APM it's best to set up a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) and add the `x-pack/plugins/apm` directory, the `x-pack` directory, and the root of the Kibana repository to the workspace. This makes it so you can navigate and search within APM and use the wider workspace roots when you need to widen your search. #### Using the Jest extension @@ -25,7 +25,7 @@ If you have a workspace configured as described above you should have: in your Workspace settings, and: ```json -"jest.pathToJest": "node scripts/jest.js --testPathPattern=legacy/plugins/apm", +"jest.pathToJest": "node scripts/jest.js --testPathPattern=plugins/apm", "jest.rootPath": "../../.." ``` @@ -40,7 +40,7 @@ To make the [VSCode debugger](https://vscode.readthedocs.io/en/latest/editor/deb "type": "node", "name": "APM Jest", "request": "launch", - "args": ["--runInBand", "--testPathPattern=legacy/plugins/apm"], + "args": ["--runInBand", "--testPathPattern=plugins/apm"], "cwd": "${workspaceFolder}/../../..", "console": "internalConsole", "internalConsoleOptions": "openOnSessionStart", diff --git a/x-pack/legacy/plugins/apm/e2e/.gitignore b/x-pack/plugins/apm/e2e/.gitignore similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/.gitignore rename to x-pack/plugins/apm/e2e/.gitignore diff --git a/x-pack/legacy/plugins/apm/e2e/README.md b/x-pack/plugins/apm/e2e/README.md similarity index 84% rename from x-pack/legacy/plugins/apm/e2e/README.md rename to x-pack/plugins/apm/e2e/README.md index a891d64539a3f..b630747ac2d3e 100644 --- a/x-pack/legacy/plugins/apm/e2e/README.md +++ b/x-pack/plugins/apm/e2e/README.md @@ -3,7 +3,7 @@ **Run E2E tests** ```sh -x-pack/legacy/plugins/apm/e2e/run-e2e.sh +x-pack/plugins/apm/e2e/run-e2e.sh ``` _Starts Kibana, APM Server, Elasticsearch (with sample data) and runs the tests_ @@ -16,9 +16,9 @@ The Jenkins CI uses a shell script to prepare Kibana: ```shell # Prepare and run Kibana locally -$ x-pack/legacy/plugins/apm/e2e/ci/prepare-kibana.sh +$ x-pack/plugins/apm/e2e/ci/prepare-kibana.sh # Build Docker image for Kibana -$ docker build --tag cypress --build-arg NODE_VERSION=$(cat .node-version) x-pack/legacy/plugins/apm/e2e/ci +$ docker build --tag cypress --build-arg NODE_VERSION=$(cat .node-version) x-pack/plugins/apm/e2e/ci # Run Docker image $ docker run --rm -t --user "$(id -u):$(id -g)" \ -v `pwd`:/app --network="host" \ diff --git a/x-pack/legacy/plugins/apm/e2e/ci/Dockerfile b/x-pack/plugins/apm/e2e/ci/Dockerfile similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/ci/Dockerfile rename to x-pack/plugins/apm/e2e/ci/Dockerfile diff --git a/x-pack/legacy/plugins/apm/e2e/ci/entrypoint.sh b/x-pack/plugins/apm/e2e/ci/entrypoint.sh similarity index 90% rename from x-pack/legacy/plugins/apm/e2e/ci/entrypoint.sh rename to x-pack/plugins/apm/e2e/ci/entrypoint.sh index ae5155d966e58..3349aa74dadb9 100755 --- a/x-pack/legacy/plugins/apm/e2e/ci/entrypoint.sh +++ b/x-pack/plugins/apm/e2e/ci/entrypoint.sh @@ -21,9 +21,9 @@ npm config set cache ${HOME} # --exclude=packages/ \ # --exclude=built_assets --exclude=target \ # --exclude=data /app ${HOME}/ -#cd ${HOME}/app/x-pack/legacy/plugins/apm/e2e/cypress +#cd ${HOME}/app/x-pack/plugins/apm/e2e/cypress -cd /app/x-pack/legacy/plugins/apm/e2e +cd /app/x-pack/plugins/apm/e2e ## Install dependencies for cypress CI=true npm install yarn install diff --git a/x-pack/legacy/plugins/apm/e2e/ci/kibana.e2e.yml b/x-pack/plugins/apm/e2e/ci/kibana.e2e.yml similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/ci/kibana.e2e.yml rename to x-pack/plugins/apm/e2e/ci/kibana.e2e.yml diff --git a/x-pack/legacy/plugins/apm/e2e/ci/prepare-kibana.sh b/x-pack/plugins/apm/e2e/ci/prepare-kibana.sh similarity index 95% rename from x-pack/legacy/plugins/apm/e2e/ci/prepare-kibana.sh rename to x-pack/plugins/apm/e2e/ci/prepare-kibana.sh index 6df17bd51e0e8..637f8fa9b4c74 100755 --- a/x-pack/legacy/plugins/apm/e2e/ci/prepare-kibana.sh +++ b/x-pack/plugins/apm/e2e/ci/prepare-kibana.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -e -E2E_DIR="x-pack/legacy/plugins/apm/e2e" +E2E_DIR="x-pack/plugins/apm/e2e" echo "1/3 Install dependencies ..." # shellcheck disable=SC1091 diff --git a/x-pack/legacy/plugins/apm/e2e/cypress.json b/x-pack/plugins/apm/e2e/cypress.json similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress.json rename to x-pack/plugins/apm/e2e/cypress.json diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/fixtures/example.json b/x-pack/plugins/apm/e2e/cypress/fixtures/example.json similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/fixtures/example.json rename to x-pack/plugins/apm/e2e/cypress/fixtures/example.json diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/integration/apm.feature b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/integration/apm.feature rename to x-pack/plugins/apm/e2e/cypress/integration/apm.feature diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/integration/helpers.ts b/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/integration/helpers.ts rename to x-pack/plugins/apm/e2e/cypress/integration/helpers.ts diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js new file mode 100644 index 0000000000000..a462f4a504145 --- /dev/null +++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js @@ -0,0 +1,16 @@ +/* + * 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. + */ + +module.exports = { + APM: { + 'Transaction duration charts': { + '1': '500 ms', + '2': '250 ms', + '3': '0 ms' + } + }, + __version: '4.2.0' +}; diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/plugins/index.js b/x-pack/plugins/apm/e2e/cypress/plugins/index.js similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/plugins/index.js rename to x-pack/plugins/apm/e2e/cypress/plugins/index.js diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/support/commands.js b/x-pack/plugins/apm/e2e/cypress/support/commands.js similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/support/commands.js rename to x-pack/plugins/apm/e2e/cypress/support/commands.js diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/support/index.ts b/x-pack/plugins/apm/e2e/cypress/support/index.ts similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/support/index.ts rename to x-pack/plugins/apm/e2e/cypress/support/index.ts diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/support/step_definitions/apm.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/support/step_definitions/apm.ts rename to x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/typings/index.d.ts b/x-pack/plugins/apm/e2e/cypress/typings/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/typings/index.d.ts rename to x-pack/plugins/apm/e2e/cypress/typings/index.d.ts diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/webpack.config.js b/x-pack/plugins/apm/e2e/cypress/webpack.config.js similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/webpack.config.js rename to x-pack/plugins/apm/e2e/cypress/webpack.config.js diff --git a/x-pack/legacy/plugins/apm/e2e/ingest-data/replay.js b/x-pack/plugins/apm/e2e/ingest-data/replay.js similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/ingest-data/replay.js rename to x-pack/plugins/apm/e2e/ingest-data/replay.js diff --git a/x-pack/legacy/plugins/apm/e2e/package.json b/x-pack/plugins/apm/e2e/package.json similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/package.json rename to x-pack/plugins/apm/e2e/package.json diff --git a/x-pack/legacy/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh similarity index 98% rename from x-pack/legacy/plugins/apm/e2e/run-e2e.sh rename to x-pack/plugins/apm/e2e/run-e2e.sh index 7c17c14dc9601..818d45abb0e65 100755 --- a/x-pack/legacy/plugins/apm/e2e/run-e2e.sh +++ b/x-pack/plugins/apm/e2e/run-e2e.sh @@ -27,7 +27,7 @@ cd ${E2E_DIR} # Ask user to start Kibana ################################################## echo "\n${bold}To start Kibana please run the following command:${normal} -node ./scripts/kibana --no-base-path --dev --no-dev-config --config x-pack/legacy/plugins/apm/e2e/ci/kibana.e2e.yml" +node ./scripts/kibana --no-base-path --dev --no-dev-config --config x-pack/plugins/apm/e2e/ci/kibana.e2e.yml" # # Create tmp folder diff --git a/x-pack/legacy/plugins/apm/e2e/tsconfig.json b/x-pack/plugins/apm/e2e/tsconfig.json similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/tsconfig.json rename to x-pack/plugins/apm/e2e/tsconfig.json diff --git a/x-pack/legacy/plugins/apm/e2e/yarn.lock b/x-pack/plugins/apm/e2e/yarn.lock similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/yarn.lock rename to x-pack/plugins/apm/e2e/yarn.lock diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 7ffdb676c740f..1a0ad67c7b696 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -1,13 +1,23 @@ { "id": "apm", - "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", - "apm" + "requiredPlugins": [ + "features", + "apm_oss", + "data", + "home", + "licensing", + "triggers_actions_ui" + ], + "optionalPlugins": [ + "cloud", + "usageCollection", + "taskManager", + "actions", + "alerting" ], - "ui": false, - "requiredPlugins": ["apm_oss", "data", "home", "licensing"], - "optionalPlugins": ["cloud", "usageCollection", "taskManager","actions", "alerting"] + "server": true, + "ui": true, + "configPath": ["xpack", "apm"] } diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx new file mode 100644 index 0000000000000..c3738329219a8 --- /dev/null +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -0,0 +1,123 @@ +/* + * 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 { ApmRoute } from '@elastic/apm-rum-react'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Route, Router, Switch } from 'react-router-dom'; +import styled from 'styled-components'; +import { CoreStart, AppMountParameters } from '../../../../../src/core/public'; +import { ApmPluginSetupDeps } from '../plugin'; +import { ApmPluginContext } from '../context/ApmPluginContext'; +import { LicenseProvider } from '../context/LicenseContext'; +import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext'; +import { LocationProvider } from '../context/LocationContext'; +import { MatchedRouteProvider } from '../context/MatchedRouteContext'; +import { UrlParamsProvider } from '../context/UrlParamsContext'; +import { AlertsContextProvider } from '../../../triggers_actions_ui/public'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { px, unit, units } from '../style/variables'; +import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; +import { APMIndicesPermission } from '../components/app/APMIndicesPermission'; +import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; +import { routes } from '../components/app/Main/route_config'; +import { history } from '../utils/history'; +import { ConfigSchema } from '..'; +import 'react-vis/dist/style.css'; + +const MainContainer = styled.div` + min-width: ${px(unit * 50)}; + padding: ${px(units.plus)}; + height: 100%; +`; + +const App = () => { + return ( + + + + + + {routes.map((route, i) => ( + + ))} + + + + ); +}; + +const ApmAppRoot = ({ + core, + deps, + routerHistory, + config +}: { + core: CoreStart; + deps: ApmPluginSetupDeps; + routerHistory: typeof history; + config: ConfigSchema; +}) => { + const i18nCore = core.i18n; + const plugins = deps; + const apmPluginContextValue = { + config, + core, + plugins + }; + return ( + + + + + + + + + + + + + + + + + + + + + + ); +}; + +/** + * This module is rendered asynchronously in the Kibana platform. + */ +export const renderApp = ( + core: CoreStart, + deps: ApmPluginSetupDeps, + { element }: AppMountParameters, + config: ConfigSchema +) => { + ReactDOM.render( + , + element + ); + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/APMIndicesPermission/__test__/APMIndicesPermission.test.tsx b/x-pack/plugins/apm/public/components/app/APMIndicesPermission/__test__/APMIndicesPermission.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/APMIndicesPermission/__test__/APMIndicesPermission.test.tsx rename to x-pack/plugins/apm/public/components/app/APMIndicesPermission/__test__/APMIndicesPermission.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/APMIndicesPermission/index.tsx b/x-pack/plugins/apm/public/components/app/APMIndicesPermission/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/APMIndicesPermission/index.tsx rename to x-pack/plugins/apm/public/components/app/APMIndicesPermission/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx index 33774c941ffd6..5982346d97b89 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; -import { APMError } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; export interface ErrorTab { key: 'log_stacktrace' | 'exception_stacktrace' | 'metadata'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.test.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx index 75e518a278aea..faec93013886c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { EuiTitle } from '@elastic/eui'; -import { Exception } from '../../../../../../../../plugins/apm/typings/es_schemas/raw/error_raw'; +import { Exception } from '../../../../../typings/es_schemas/raw/error_raw'; import { Stacktrace } from '../../../shared/Stacktrace'; import { CauseStacktrace } from '../../../shared/Stacktrace/CauseStacktrace'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 490bf472065e3..9e2fd776e67a3 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -20,8 +20,8 @@ import React from 'react'; import styled from 'styled-components'; import { first } from 'lodash'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ErrorGroupAPIResponse } from '../../../../../../../../plugins/apm/server/lib/errors/get_error_group'; -import { APMError } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +import { ErrorGroupAPIResponse } from '../../../../../server/lib/errors/get_error_group'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { px, unit, units } from '../../../../style/variables'; import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index ccd720ceee075..c40c711a590be 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -17,7 +17,7 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../plugins/apm/common/i18n'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; import { useFetcher } from '../../../hooks/useFetcher'; import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables'; import { ApmHeader } from '../../shared/ApmHeader'; @@ -25,7 +25,7 @@ import { DetailView } from './DetailView'; import { ErrorDistribution } from './Distribution'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; +import { useTrackPageview } from '../../../../../observability/public'; const Titles = styled.div` margin-bottom: ${px(units.plus)}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index 250b9a5d188d0..695d3463d3b3d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -9,9 +9,9 @@ import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../../plugins/apm/common/i18n'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ErrorGroupListAPIResponse } from '../../../../../../../../plugins/apm/server/lib/errors/get_error_groups'; +import { ErrorGroupListAPIResponse } from '../../../../../server/lib/errors/get_error_groups'; import { fontFamilyCode, fontSizes, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx index 8c5a4545f1043..604893952d9d6 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -17,8 +17,8 @@ import { useFetcher } from '../../../hooks/useFetcher'; import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; import { ErrorGroupList } from './List'; import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; +import { useTrackPageview } from '../../../../../observability/public'; +import { PROJECTION } from '../../../../common/projections/typings'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; const ErrorGroupOverview: React.FC = () => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx b/x-pack/plugins/apm/public/components/app/Home/Home.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx rename to x-pack/plugins/apm/public/components/app/Home/Home.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap index 2b1f835a14f4a..9f461eeb5b6fc 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap @@ -5,7 +5,6 @@ exports[`Home component should render services 1`] = ` value={ Object { "config": Object { - "indexPatternTitle": "apm-*", "serviceMapEnabled": true, "ui": Object { "enabled": false, @@ -46,7 +45,6 @@ exports[`Home component should render traces 1`] = ` value={ Object { "config": Object { - "indexPatternTitle": "apm-*", "serviceMapEnabled": true, "ui": Object { "enabled": false, diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx rename to x-pack/plugins/apm/public/components/app/Home/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx rename to x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/ScrollToTopOnPathChange.tsx b/x-pack/plugins/apm/public/components/app/Main/ScrollToTopOnPathChange.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/ScrollToTopOnPathChange.tsx rename to x-pack/plugins/apm/public/components/app/Main/ScrollToTopOnPathChange.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx rename to x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx rename to x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/ProvideBreadcrumbs.test.tsx b/x-pack/plugins/apm/public/components/app/Main/__test__/ProvideBreadcrumbs.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/__test__/ProvideBreadcrumbs.test.tsx rename to x-pack/plugins/apm/public/components/app/Main/__test__/ProvideBreadcrumbs.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx rename to x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index c87e56fe9eff6..6d1db8c5dc6d4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; -import { SERVICE_NODE_NAME_MISSING } from '../../../../../../../../plugins/apm/common/service_nodes'; +import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes'; import { ErrorGroupDetails } from '../../ErrorGroupDetails'; import { ServiceDetails } from '../../ServiceDetails'; import { TransactionDetails } from '../../TransactionDetails'; @@ -20,7 +20,7 @@ import { ApmIndices } from '../../Settings/ApmIndices'; import { toQuery } from '../../../shared/Links/url_helpers'; import { ServiceNodeMetrics } from '../../ServiceNodeMetrics'; import { resolveUrlParams } from '../../../../context/UrlParamsContext/resolveUrlParams'; -import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../../../../plugins/apm/common/i18n'; +import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n'; import { TraceLink } from '../../TraceLink'; import { CustomizeUI } from '../../Settings/CustomizeUI'; import { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx rename to x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx rename to x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx similarity index 82% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx index 7e8d057a7be6c..a1ccb04e3c42a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { AlertType } from '../../../../../../../../../plugins/apm/common/alert_types'; -import { AlertAdd } from '../../../../../../../../../plugins/triggers_actions_ui/public'; +import { AlertType } from '../../../../../../common/alert_types'; +import { AlertAdd } from '../../../../../../../triggers_actions_ui/public'; type AlertAddProps = React.ComponentProps; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx index 92b325ab00d35..75c6c79bc804a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx @@ -12,7 +12,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; -import { AlertType } from '../../../../../../../../plugins/apm/common/alert_types'; +import { AlertType } from '../../../../../common/alert_types'; import { AlertingFlyout } from './AlertingFlyout'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index 131bb7f65d4b3..7ab2f7bac8ae2 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -7,10 +7,7 @@ import { EuiTabs } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { - isJavaAgentName, - isRumAgentName -} from '../../../../../../../plugins/apm/common/agent_name'; +import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name'; import { useAgentName } from '../../../hooks/useAgentName'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useUrlParams } from '../../../hooks/useUrlParams'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx index cc5c62e25b491..b7480a42ba94b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import React, { Component } from 'react'; -import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import { startMLJob } from '../../../../../services/rest/ml'; import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index 85254bee12e13..3bbd8a01d0549 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -30,12 +30,13 @@ import { padLeft, range } from 'lodash'; import moment from 'moment-timezone'; import React, { Component } from 'react'; import styled from 'styled-components'; -import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { KibanaLink } from '../../../shared/Links/KibanaLink'; import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch'; import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; import { ApmPluginContext } from '../../../../context/ApmPluginContext'; +import { getApmIndexPatternTitle } from '../../../../services/rest/index_pattern'; type ScheduleKey = keyof Schedule; @@ -149,11 +150,7 @@ export class WatcherFlyout extends Component< this.setState({ slackUrl: event.target.value }); }; - public createWatch = ({ - indexPatternTitle - }: { - indexPatternTitle: string; - }) => () => { + public createWatch = () => { const { serviceName } = this.props.urlParams; const { core } = this.context; @@ -190,19 +187,21 @@ export class WatcherFlyout extends Component< unit: 'h' }; - return createErrorGroupWatch({ - http: core.http, - emails, - schedule, - serviceName, - slackUrl, - threshold: this.state.threshold, - timeRange, - apmIndexPatternTitle: indexPatternTitle - }) - .then((id: string) => { - this.props.onClose(); - this.addSuccessToast(id); + return getApmIndexPatternTitle() + .then(indexPatternTitle => { + return createErrorGroupWatch({ + http: core.http, + emails, + schedule, + serviceName, + slackUrl, + threshold: this.state.threshold, + timeRange, + apmIndexPatternTitle: indexPatternTitle + }).then((id: string) => { + this.props.onClose(); + this.addSuccessToast(id); + }); }) .catch(e => { // eslint-disable-next-line @@ -613,26 +612,20 @@ export class WatcherFlyout extends Component< - - {({ config }) => { - return ( - - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.createWatchButtonLabel', - { - defaultMessage: 'Create watch' - } - )} - - ); - }} - + this.createWatch()} + fill + disabled={ + !this.state.actions.email && !this.state.actions.slack + } + > + {i18n.translate( + 'xpack.apm.serviceDetails.enableErrorReportsPanel.createWatchButtonLabel', + { + defaultMessage: 'Create watch' + } + )} + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts index 690db9fcdd8d6..d45453e24f1c9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts @@ -17,7 +17,7 @@ import { ERROR_LOG_MESSAGE, PROCESSOR_EVENT, SERVICE_NAME -} from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../../../common/elasticsearch_fieldnames'; import { createWatch } from '../../../../services/rest/watcher'; function getSlackPathUrl(slackUrl?: string) { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index 53c86f92ee557..ad77434bca9f4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -19,7 +19,7 @@ import { cytoscapeOptions, nodeHeight } from './cytoscapeOptions'; -import { useUiTracker } from '../../../../../../../plugins/observability/public'; +import { useUiTracker } from '../../../../../observability/public'; export const CytoscapeContext = createContext( undefined diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 491ebdc5aad15..bc3434f277d1c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -12,7 +12,7 @@ import { } from '@elastic/eui'; import cytoscape from 'cytoscape'; import React from 'react'; -import { SERVICE_FRAMEWORK_NAME } from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +import { SERVICE_FRAMEWORK_NAME } from '../../../../../common/elasticsearch_fieldnames'; import { Buttons } from './Buttons'; import { Info } from './Info'; import { ServiceMetricFetcher } from './ServiceMetricFetcher'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index e1df3b474e9de..541f4f6a1e775 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { SPAN_SUBTYPE, SPAN_TYPE -} from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../../../common/elasticsearch_fieldnames'; const ItemRow = styled.div` line-height: 2; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx index 697aa6a1b652b..5e6412333a2e1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { ServiceNodeMetrics } from '../../../../../../../../plugins/apm/common/service_map'; +import { ServiceNodeMetrics } from '../../../../../common/service_map'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { ServiceMetricList } from './ServiceMetricList'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index 056af68cc8173..3cee986261a68 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; import styled from 'styled-components'; -import { ServiceNodeMetrics } from '../../../../../../../../plugins/apm/common/service_map'; +import { ServiceNodeMetrics } from '../../../../../common/service_map'; import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters'; function LoadingSpinner() { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx index 102b135f3cd1f..1c9d5092bfcf5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx @@ -14,7 +14,7 @@ import React, { useRef, useState } from 'react'; -import { SERVICE_NAME } from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; import { CytoscapeContext } from '../Cytoscape'; import { Contents } from './Contents'; import { animationOptions } from '../cytoscapeOptions'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json rename to x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts rename to x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index e9942a327b69e..554f84f0ad236 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -9,7 +9,7 @@ import { CSSProperties } from 'react'; import { SERVICE_NAME, SPAN_DESTINATION_SERVICE_RESOURCE -} from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../../common/elasticsearch_fieldnames'; import { defaultIcon, iconForNode } from './icons'; // IE 11 does not properly load some SVGs or draw certain shapes. This causes diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts index 321b39dabbbd5..9fe5cbd23b07c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -5,12 +5,12 @@ */ import cytoscape from 'cytoscape'; -import { isRumAgentName } from '../../../../../../../plugins/apm/common/agent_name'; +import { isRumAgentName } from '../../../../common/agent_name'; import { AGENT_NAME, SPAN_SUBTYPE, SPAN_TYPE -} from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../../common/elasticsearch_fieldnames'; import awsIcon from './icons/aws.svg'; import cassandraIcon from './icons/cassandra.svg'; import darkIcon from './icons/dark.svg'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/aws.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/aws.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/aws.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/aws.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dark.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dark.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/database.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/database.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/database.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/database.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/default.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/default.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/documents.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/documents.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/documents.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/documents.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/globe.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/globe.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/globe.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/globe.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/go.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/go.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/java.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/java.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/php.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/php.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/python.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/python.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/redis.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/redis.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/redis.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/redis.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx index d93caa601f0b6..c7e25269511bf 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -6,7 +6,7 @@ import { render } from '@testing-library/react'; import React, { FunctionComponent } from 'react'; -import { License } from '../../../../../../../plugins/licensing/common/license'; +import { License } from '../../../../../licensing/common/license'; import { LicenseContext } from '../../../context/LicenseContext'; import { ServiceMap } from './'; import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 94e42f1b91160..b57f0b047c613 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { invalidLicenseMessage, isValidPlatinumLicense -} from '../../../../../../../plugins/apm/common/service_map'; +} from '../../../../common/service_map'; import { useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -23,7 +23,7 @@ import { EmptyBanner } from './EmptyBanner'; import { Popover } from './Popover'; import { useRefDimensions } from './useRefDimensions'; import { BetaBadge } from './BetaBadge'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; +import { useTrackPageview } from '../../../../../observability/public'; interface ServiceMapProps { serviceName?: string; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts rename to x-pack/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx index 060e635e83549..0fb8c00a2b162 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx @@ -16,7 +16,7 @@ import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; import { MetricsChart } from '../../shared/charts/MetricsChart'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; +import { PROJECTION } from '../../../../common/projections/typings'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; interface ServiceMetricsProps { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx rename to x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx index 2bf26946932ea..3929c153ae419 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SERVICE_NODE_NAME_MISSING } from '../../../../../../../plugins/apm/common/service_nodes'; +import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { ApmHeader } from '../../shared/ApmHeader'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useAgentName } from '../../../hooks/useAgentName'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index 3af1a70ef3fdc..4e57cb47691be 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -13,9 +13,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; -import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../../../plugins/apm/common/i18n'; -import { SERVICE_NODE_NAME_MISSING } from '../../../../../../../plugins/apm/common/service_nodes'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; +import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; +import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; +import { PROJECTION } from '../../../../common/projections/typings'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { ManagedTable, ITableColumn } from '../../shared/ManagedTable'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx rename to x-pack/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index 1ac29c5626e3a..7e2d03ad35899 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceListAPIResponse } from '../../../../../../../../plugins/apm/server/lib/services/get_services'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../../plugins/apm/common/i18n'; +import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { fontSizes, truncate } from '../../../../style/variables'; import { asDecimal, convertTo } from '../../../../utils/formatters'; import { ManagedTable } from '../../../shared/ManagedTable'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx rename to x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx rename to x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx index 52bc414a93a23..99b169e3ec361 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -9,13 +9,13 @@ import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useMemo } from 'react'; import url from 'url'; -import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { useFetcher } from '../../../hooks/useFetcher'; import { NoServicesMessage } from './NoServicesMessage'; import { ServiceList } from './ServiceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; +import { useTrackPageview } from '../../../../../observability/public'; +import { PROJECTION } from '../../../../common/projections/typings'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx index 43002c79aa2b4..f4b942c7f46eb 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx @@ -16,11 +16,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { isString } from 'lodash'; import { EuiButtonEmpty } from '@elastic/eui'; -import { AgentConfigurationIntake } from '../../../../../../../../../../plugins/apm/common/agent_configuration/configuration_types'; +import { AgentConfigurationIntake } from '../../../../../../../common/agent_configuration/configuration_types'; import { omitAllOption, getOptionLabel -} from '../../../../../../../../../../plugins/apm/common/agent_configuration/all_option'; +} from '../../../../../../../common/agent_configuration/all_option'; import { useFetcher, FETCH_STATUS } from '../../../../../../hooks/useFetcher'; import { FormRowSelect } from './FormRowSelect'; import { APMLink } from '../../../../../shared/Links/apm/APMLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx index baab600145b81..fcd75a05b01d9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx @@ -17,12 +17,12 @@ import { EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SettingDefinition } from '../../../../../../../../../../plugins/apm/common/agent_configuration/setting_definitions/types'; -import { isValid } from '../../../../../../../../../../plugins/apm/common/agent_configuration/setting_definitions'; +import { SettingDefinition } from '../../../../../../../common/agent_configuration/setting_definitions/types'; +import { isValid } from '../../../../../../../common/agent_configuration/setting_definitions'; import { amountAndUnitToString, amountAndUnitToObject -} from '../../../../../../../../../../plugins/apm/common/agent_configuration/amount_and_unit'; +} from '../../../../../../../common/agent_configuration/amount_and_unit'; import { SelectWithPlaceholder } from '../../../../../shared/SelectWithPlaceholder'; function FormRow({ diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx index 6d76b69600333..e41bdaf0c9c09 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx @@ -23,19 +23,19 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty } from '@elastic/eui'; import { EuiCallOut } from '@elastic/eui'; import { FETCH_STATUS } from '../../../../../../hooks/useFetcher'; -import { AgentName } from '../../../../../../../../../../plugins/apm/typings/es_schemas/ui/fields/agent'; +import { AgentName } from '../../../../../../../typings/es_schemas/ui/fields/agent'; import { history } from '../../../../../../utils/history'; -import { AgentConfigurationIntake } from '../../../../../../../../../../plugins/apm/common/agent_configuration/configuration_types'; +import { AgentConfigurationIntake } from '../../../../../../../common/agent_configuration/configuration_types'; import { filterByAgent, settingDefinitions, isValid -} from '../../../../../../../../../../plugins/apm/common/agent_configuration/setting_definitions'; +} from '../../../../../../../common/agent_configuration/setting_definitions'; import { saveConfig } from './saveConfig'; import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; -import { useUiTracker } from '../../../../../../../../../../plugins/observability/public'; +import { useUiTracker } from '../../../../../../../../observability/public'; import { SettingFormRow } from './SettingFormRow'; -import { getOptionLabel } from '../../../../../../../../../../plugins/apm/common/agent_configuration/all_option'; +import { getOptionLabel } from '../../../../../../../common/agent_configuration/all_option'; function removeEmpty(obj: T): T { return Object.fromEntries( diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts similarity index 90% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts index 7e3bcd68699be..5f7354bf6f713 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts @@ -6,11 +6,11 @@ import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; -import { AgentConfigurationIntake } from '../../../../../../../../../../plugins/apm/common/agent_configuration/configuration_types'; +import { AgentConfigurationIntake } from '../../../../../../../common/agent_configuration/configuration_types'; import { getOptionLabel, omitAllOption -} from '../../../../../../../../../../plugins/apm/common/agent_configuration/all_option'; +} from '../../../../../../../common/agent_configuration/all_option'; import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; export async function saveConfig({ diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx index 531e557b6ef86..089bc58f50a88 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx @@ -13,7 +13,7 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import { HttpSetup } from 'kibana/public'; -import { AgentConfiguration } from '../../../../../../../../../plugins/apm/common/agent_configuration/configuration_types'; +import { AgentConfiguration } from '../../../../../../common/agent_configuration/configuration_types'; import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; import { createCallApmApi } from '../../../../../services/rest/createCallApmApi'; import { AgentConfigurationCreateEdit } from './index'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx index 638e518563f8c..3a6f94b975800 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx @@ -13,7 +13,7 @@ import { history } from '../../../../../utils/history'; import { AgentConfigurationIntake, AgentConfiguration -} from '../../../../../../../../../plugins/apm/common/agent_configuration/configuration_types'; +} from '../../../../../../common/agent_configuration/configuration_types'; import { ServicePage } from './ServicePage/ServicePage'; import { SettingsPage } from './SettingsPage/SettingsPage'; import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx index 267aaddc93f76..6a1a472562305 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx @@ -9,8 +9,8 @@ import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { NotificationsStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AgentConfigurationListAPIResponse } from '../../../../../../../../../plugins/apm/server/lib/settings/agent_configuration/list_configurations'; -import { getOptionLabel } from '../../../../../../../../../plugins/apm/common/agent_configuration/all_option'; +import { AgentConfigurationListAPIResponse } from '../../../../../../server/lib/settings/agent_configuration/list_configurations'; +import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; import { callApmApi } from '../../../../../services/rest/createCallApmApi'; import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx index 6d5f65121d8fd..9eaa7786baca0 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx @@ -20,10 +20,10 @@ import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; import { ITableColumn, ManagedTable } from '../../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../../shared/LoadingStatePrompt'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AgentConfigurationListAPIResponse } from '../../../../../../../../../plugins/apm/server/lib/settings/agent_configuration/list_configurations'; +import { AgentConfigurationListAPIResponse } from '../../../../../../server/lib/settings/agent_configuration/list_configurations'; import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; import { px, units } from '../../../../../style/variables'; -import { getOptionLabel } from '../../../../../../../../../plugins/apm/common/agent_configuration/all_option'; +import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; import { createAgentConfigurationHref, editAgentConfigurationHref diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx index 8171e339adc82..4349e542449cc 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -17,7 +17,7 @@ import { import { isEmpty } from 'lodash'; import { useFetcher } from '../../../../hooks/useFetcher'; import { AgentConfigurationList } from './List'; -import { useTrackPageview } from '../../../../../../../../plugins/observability/public'; +import { useTrackPageview } from '../../../../../../observability/public'; import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks'; export function AgentConfigurations() { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx rename to x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx index fb8ffe6722c87..9c244e3cde411 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { Filter, FilterKey -} from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +} from '../../../../../../../common/custom_link/custom_link_types'; import { DEFAULT_OPTION, FILTER_SELECT_OPTIONS, diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.test.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx index 8edfb176a1af8..8fed838a48261 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx @@ -17,8 +17,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { debounce } from 'lodash'; -import { Filter } from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Filter } from '../../../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; import { replaceTemplateVariables, convertFiltersToQuery } from './helper'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx index 630f7148ad408..210033888d90c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx @@ -12,7 +12,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { CustomLink } from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { CustomLink } from '../../../../../../../common/custom_link/custom_link_types'; import { Documentation } from './Documentation'; interface InputField { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts index 0a63cfcff9aa5..49e381aab675d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts @@ -7,7 +7,7 @@ import { getSelectOptions, replaceTemplateVariables } from '../CustomLinkFlyout/helper'; -import { Transaction } from '../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; describe('Custom link helper', () => { describe('getSelectOptions', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts similarity index 91% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts index 7bfdbf1655e0d..8c35b8fe77506 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts @@ -6,12 +6,12 @@ import { i18n } from '@kbn/i18n'; import Mustache from 'mustache'; import { isEmpty, get } from 'lodash'; -import { FILTER_OPTIONS } from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_filter_options'; +import { FILTER_OPTIONS } from '../../../../../../../common/custom_link/custom_link_filter_options'; import { Filter, FilterKey -} from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +} from '../../../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; interface FilterSelectOption { value: 'DEFAULT' | FilterKey; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx index 0b25a0a79edd9..150147d9af405 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx @@ -14,7 +14,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; -import { Filter } from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { Filter } from '../../../../../../../common/custom_link/custom_link_types'; import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; import { FiltersSection } from './FiltersSection'; import { FlyoutFooter } from './FlyoutFooter'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts index 9cbaf16320a6b..685b3ab022950 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts @@ -9,7 +9,7 @@ import { NotificationsStart } from 'kibana/public'; import { Filter, CustomLink -} from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +} from '../../../../../../../common/custom_link/custom_link_types'; import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; export async function saveCustomLink({ diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx index 68e6ee52af0b0..d68fb757e53d1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx @@ -13,7 +13,7 @@ import { EuiSpacer } from '@elastic/eui'; import { isEmpty } from 'lodash'; -import { CustomLink } from '../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; import { units, px } from '../../../../../style/variables'; import { ManagedTable } from '../../../../shared/ManagedTable'; import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx similarity index 99% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index e5c20b260e097..32a08f5ffaf7c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -8,7 +8,7 @@ import { fireEvent, render, wait, RenderResult } from '@testing-library/react'; import React from 'react'; import { act } from 'react-dom/test-utils'; import * as apmApi from '../../../../../services/rest/createCallApmApi'; -import { License } from '../../../../../../../../../plugins/licensing/common/license'; +import { License } from '../../../../../../../licensing/common/license'; import * as hooks from '../../../../../hooks/useFetcher'; import { LicenseContext } from '../../../../../context/LicenseContext'; import { CustomLinkOverview } from '.'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index e9a915e0f59bc..b94ce513bc210 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -8,7 +8,7 @@ import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty } from 'lodash'; import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { CustomLink } from '../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; import { useLicense } from '../../../../../hooks/useLicense'; import { useFetcher, FETCH_STATUS } from '../../../../../hooks/useFetcher'; import { CustomLinkFlyout } from './CustomLinkFlyout'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx rename to x-pack/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/TraceLink/index.tsx rename to x-pack/plugins/apm/public/components/app/TraceLink/index.tsx index f0301f8917d10..04d830f4649d4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { Redirect } from 'react-router-dom'; import styled from 'styled-components'; import url from 'url'; -import { TRACE_ID } from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { TRACE_ID } from '../../../../common/elasticsearch_fieldnames'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; import { useUrlParams } from '../../../hooks/useUrlParams'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx rename to x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index 91f3051acf077..92d5a38cc11ca 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ITransactionGroup } from '../../../../../../../plugins/apm/server/lib/transaction_groups/transform'; +import { ITransactionGroup } from '../../../../server/lib/transaction_groups/transform'; import { fontSizes, truncate } from '../../../style/variables'; import { convertTo } from '../../../utils/formatters'; import { EmptyMessage } from '../../shared/EmptyMessage'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx similarity index 91% rename from x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx rename to x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx index bfbad78a5c026..a7fa927f9e9b1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -9,9 +9,9 @@ import React, { useMemo } from 'react'; import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; import { TraceList } from './TraceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; +import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; +import { PROJECTION } from '../../../../common/projections/typings'; export function TraceOverview() { const { urlParams, uiFilters } = useUrlParams(); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts index 08682fb3be842..7ad0a77505b9d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts @@ -6,7 +6,7 @@ import { getFormattedBuckets } from '../index'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IBucket } from '../../../../../../../../../plugins/apm/server/lib/transactions/distribution/get_buckets/transform'; +import { IBucket } from '../../../../../../server/lib/transactions/distribution/get_buckets/transform'; describe('Distribution', () => { it('getFormattedBuckets', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index e70133aabb679..b7dbfbdbd7d7e 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -10,9 +10,9 @@ import d3 from 'd3'; import React, { FunctionComponent, useCallback } from 'react'; import { isEmpty } from 'lodash'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionDistributionAPIResponse } from '../../../../../../../../plugins/apm/server/lib/transactions/distribution'; +import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IBucket } from '../../../../../../../../plugins/apm/server/lib/transactions/distribution/get_buckets/transform'; +import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { getDurationFormatter } from '../../../../utils/formatters'; // @ts-ignore diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx index 4e105957f5f9d..1db8e02e38692 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiButton, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Transaction as ITransaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction as ITransaction } from '../../../../../typings/es_schemas/ui/transaction'; import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx index 9026dd90ddceb..27e0584c696c1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx @@ -8,7 +8,7 @@ import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Location } from 'history'; import React from 'react'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { history } from '../../../../utils/history'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts index 030729522f35e..ae908b25cc615 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { getAgentMarks } from '../get_agent_marks'; describe('getAgentMarks', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts similarity index 87% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts index 1dcb1598662c9..2bc64e30b4f7e 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts @@ -5,7 +5,7 @@ */ import { sortBy } from 'lodash'; -import { Transaction } from '../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; import { Mark } from '.'; // Extends Mark without adding new properties to it. diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts similarity index 89% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts index a9694efcbcae7..ad54cec5c26a7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { isEmpty } from 'lodash'; -import { ErrorRaw } from '../../../../../../../../../../plugins/apm/typings/es_schemas/raw/error_raw'; +import { ErrorRaw } from '../../../../../../../typings/es_schemas/raw/error_raw'; import { IWaterfallError, IServiceColors diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx similarity index 90% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx index 6e58dbc5b6ea3..bbc457450e475 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { SERVICE_NAME, TRANSACTION_NAME -} from '../../../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +} from '../../../../../../../common/elasticsearch_fieldnames'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; import { TransactionDetailLink } from '../../../../../shared/Links/apm/TransactionDetailLink'; import { StickyProperties } from '../../../../../shared/StickyProperties'; import { TransactionOverviewLink } from '../../../../../shared/Links/apm/TransactionOverviewLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx index 6200d5f098ad5..7a08a84bf30ba 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx @@ -18,7 +18,7 @@ import SyntaxHighlighter, { // @ts-ignore import { xcode } from 'react-syntax-highlighter/dist/styles'; import styled from 'styled-components'; -import { Span } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; +import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { borderRadius, fontFamilyCode, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx index 438e88df3351d..28564481074fa 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx @@ -17,7 +17,7 @@ import { unit, units } from '../../../../../../../style/variables'; -import { Span } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; +import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; const ContextUrl = styled.div` padding: ${px(units.half)} ${px(unit)}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx similarity index 86% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx index 621497a0b22e0..d49959c5cbffb 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx @@ -6,14 +6,14 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { SPAN_NAME, TRANSACTION_NAME, SERVICE_NAME -} from '../../../../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../../../../../plugins/apm/common/i18n'; -import { Span } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; +} from '../../../../../../../../common/elasticsearch_fieldnames'; +import { NOT_AVAILABLE_LABEL } from '../../../../../../../../common/i18n'; +import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { StickyProperties } from '../../../../../../shared/StickyProperties'; import { TransactionOverviewLink } from '../../../../../../shared/Links/apm/TransactionOverviewLink'; import { TransactionDetailLink } from '../../../../../../shared/Links/apm/TransactionDetailLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx index f57ddb5cf69a2..1da22516629f2 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -25,8 +25,8 @@ import { px, units } from '../../../../../../../style/variables'; import { Summary } from '../../../../../../shared/Summary'; import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip'; import { DurationSummaryItem } from '../../../../../../shared/Summary/DurationSummaryItem'; -import { Span } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { DiscoverSpanLink } from '../../../../../../shared/Links/DiscoverLinks/DiscoverSpanLink'; import { Stacktrace } from '../../../../../../shared/Stacktrace'; import { ResponsiveFlyout } from '../ResponsiveFlyout'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx index 85cf0b642530f..87ecb96f74735 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx @@ -7,7 +7,7 @@ import { EuiCallOut, EuiHorizontalRule } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { ElasticDocsLink } from '../../../../../../shared/Links/ElasticDocsLink'; export function DroppedSpansWarning({ diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx index e24414bb28d52..5fb679818f0a7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { TransactionActionMenu } from '../../../../../../shared/TransactionActionMenu/TransactionActionMenu'; import { TransactionSummary } from '../../../../../../shared/Summary/TransactionSummary'; import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index 5c6e0cc5ce435..d8edcce46c2d7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -10,13 +10,13 @@ import styled from 'styled-components'; import { EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import { isRumAgentName } from '../../../../../../../../../../plugins/apm/common/agent_name'; +import { isRumAgentName } from '../../../../../../../common/agent_name'; import { px, unit, units } from '../../../../../../style/variables'; import { asDuration } from '../../../../../../utils/formatters'; import { ErrorCount } from '../../ErrorCount'; import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; -import { TRACE_ID } from '../../../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +import { TRACE_ID } from '../../../../../../../common/elasticsearch_fieldnames'; import { SyncBadge } from './SyncBadge'; import { Margins } from '../../../../../shared/charts/Timeline'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts index e0a01e9422c85..75304932ed2ba 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -5,8 +5,8 @@ */ import { groupBy } from 'lodash'; -import { Span } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { getClockSkew, getOrderedWaterfallItems, @@ -15,7 +15,7 @@ import { IWaterfallTransaction, IWaterfallError } from './waterfall_helpers'; -import { APMError } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error'; describe('waterfall_helpers', () => { describe('getWaterfall', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 73193cc7c9dbb..8ddce66f0b853 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -16,10 +16,10 @@ import { zipObject } from 'lodash'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TraceAPIResponse } from '../../../../../../../../../../../plugins/apm/server/lib/traces/get_trace'; -import { APMError } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; -import { Span } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { TraceAPIResponse } from '../../../../../../../../server/lib/traces/get_trace'; +import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error'; +import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; interface IWaterfallGroup { [key: string]: IWaterfallItem[]; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx index f681f4dfc675a..87710fb9b8d96 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TraceAPIResponse } from '../../../../../../../../../plugins/apm/server/lib/traces/get_trace'; +import { TraceAPIResponse } from '../../../../../../server/lib/traces/get_trace'; import { WaterfallContainer } from './index'; import { location, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index 1082052c6929d..056e9cdb75148 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n'; import { Location } from 'history'; import React, { useEffect, useState } from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IBucket } from '../../../../../../../../plugins/apm/server/lib/transactions/distribution/get_buckets/transform'; +import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { history } from '../../../../utils/history'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx index e2634be0e0be8..2544dc2a1a77c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -26,8 +26,8 @@ import { useUrlParams } from '../../../hooks/useUrlParams'; import { FETCH_STATUS } from '../../../hooks/useFetcher'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; +import { useTrackPageview } from '../../../../../observability/public'; +import { PROJECTION } from '../../../../common/projections/typings'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { HeightRetainer } from '../../shared/HeightRetainer'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx rename to x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx index 16fda7c600906..e3b33f11d0805 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx @@ -8,9 +8,9 @@ import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../../plugins/apm/common/i18n'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ITransactionGroup } from '../../../../../../../../plugins/apm/server/lib/transaction_groups/transform'; +import { ITransactionGroup } from '../../../../../server/lib/transaction_groups/transform'; import { fontFamilyCode, truncate } from '../../../../style/variables'; import { asDecimal, convertTo } from '../../../../utils/formatters'; import { ImpactBar } from '../../../shared/ImpactBar'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx rename to x-pack/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx rename to x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index b008c98417867..60aac3fcdfeef 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -27,10 +27,10 @@ import { getHasMLJob } from '../../../services/rest/ml'; import { history } from '../../../utils/history'; import { useLocation } from '../../../hooks/useLocation'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; +import { useTrackPageview } from '../../../../../observability/public'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; +import { PROJECTION } from '../../../../common/projections/typings'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/useRedirect.ts b/x-pack/plugins/apm/public/components/app/TransactionOverview/useRedirect.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/useRedirect.ts rename to x-pack/plugins/apm/public/components/app/TransactionOverview/useRedirect.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ApmHeader/index.tsx rename to x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx rename to x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/DatePicker/index.tsx rename to x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/EmptyMessage.tsx b/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/EmptyMessage.tsx rename to x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx rename to x-pack/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx rename to x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx index e911011f0979c..d17b3b7689b19 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx @@ -15,7 +15,7 @@ import { fromQuery, toQuery } from '../Links/url_helpers'; import { ENVIRONMENT_ALL, ENVIRONMENT_NOT_DEFINED -} from '../../../../../../../plugins/apm/common/environment_filter_values'; +} from '../../../../common/environment_filter_values'; function updateEnvironmentUrl( location: ReturnType, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx rename to x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx index b7e23c2979cb8..658def7ddbb57 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { EuiFieldNumber } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isFinite } from 'lodash'; -import { ForLastExpression } from '../../../../../../../plugins/triggers_actions_ui/public'; -import { ALERT_TYPES_CONFIG } from '../../../../../../../plugins/apm/common/alert_types'; +import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; +import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ErrorStatePrompt.tsx b/x-pack/plugins/apm/public/components/shared/ErrorStatePrompt.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ErrorStatePrompt.tsx rename to x-pack/plugins/apm/public/components/shared/ErrorStatePrompt.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/EuiTabLink.tsx b/x-pack/plugins/apm/public/components/shared/EuiTabLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/EuiTabLink.tsx rename to x-pack/plugins/apm/public/components/shared/EuiTabLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/HeightRetainer/index.tsx b/x-pack/plugins/apm/public/components/shared/HeightRetainer/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/HeightRetainer/index.tsx rename to x-pack/plugins/apm/public/components/shared/HeightRetainer/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js b/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js rename to x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap b/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap rename to x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ImpactBar/index.tsx b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ImpactBar/index.tsx rename to x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx rename to x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx index 4de07f75ff84f..d33960fe5196b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx +++ b/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx @@ -8,7 +8,7 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { isBoolean, isNumber, isObject } from 'lodash'; import React from 'react'; import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../plugins/apm/common/i18n'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; const EmptyValue = styled.span` color: ${theme.euiColorMediumShade}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx rename to x-pack/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/index.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/index.tsx rename to x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js rename to x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js index 5ad256efe0945..93a95c844a975 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js @@ -27,6 +27,7 @@ export default class ClickOutside extends Component { }; render() { + // eslint-disable-next-line no-unused-vars const { onClickOutside, ...restProps } = this.props; return (
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js rename to x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js rename to x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js rename to x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts rename to x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts index 19a9ae9538ad6..f4628524cced5 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../../../../../plugins/apm/typings/elasticsearch'; +import { ESFilter } from '../../../../typings/elasticsearch'; import { TRANSACTION_TYPE, ERROR_GROUP_ID, PROCESSOR_EVENT, TRANSACTION_NAME, SERVICE_NAME -} from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../../common/elasticsearch_fieldnames'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; export function getBoolFilter(urlParams: IUrlParams) { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx rename to x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index dba31822dd23e..2622d08d4779d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -21,7 +21,7 @@ import { QuerySuggestion, esKuery, IIndexPattern -} from '../../../../../../../../src/plugins/data/public'; +} from '../../../../../../../src/plugins/data/public'; const Container = styled.div` margin-bottom: 10px; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx rename to x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LicensePrompt/index.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LicensePrompt/index.tsx rename to x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx similarity index 85% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx index 806d9f73369c3..05e4080d5d0b7 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { ERROR_GROUP_ID, SERVICE_NAME -} from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { APMError } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +} from '../../../../../common/elasticsearch_fieldnames'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import { DiscoverLink } from './DiscoverLink'; function getDiscoverQuery(error: APMError, kuery?: string) { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx index 6dc93292956fa..b58a450d26644 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx @@ -11,7 +11,7 @@ import url from 'url'; import rison, { RisonValue } from 'rison-node'; import { useLocation } from '../../../../hooks/useLocation'; import { getTimepickerRisonData } from '../rison_helpers'; -import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../plugins/apm/common/index_pattern_constants'; +import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../common/index_pattern_constants'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { AppMountContextBasePath } from '../../../../context/ApmPluginContext'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx similarity index 79% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx index 8fe5be28def22..ac9e33b3acd69 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; -import { SPAN_ID } from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { Span } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; +import { SPAN_ID } from '../../../../../common/elasticsearch_fieldnames'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; import { DiscoverLink } from './DiscoverLink'; function getDiscoverQuery(span: Span) { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx similarity index 85% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx index b0af33fd7d7f7..a5f4df7dbac1b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx @@ -9,8 +9,8 @@ import { PROCESSOR_EVENT, TRACE_ID, TRANSACTION_ID -} from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +} from '../../../../../common/elasticsearch_fieldnames'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { DiscoverLink } from './DiscoverLink'; export function getDiscoverQuery(transaction: Transaction) { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx index eeb9fd20a4bcb..acf8d89432b23 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx @@ -6,7 +6,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { APMError } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; import { DiscoverErrorLink } from '../DiscoverErrorLink'; describe('DiscoverErrorLink without kuery', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx index eeb9fd20a4bcb..acf8d89432b23 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx @@ -6,7 +6,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { APMError } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; import { DiscoverErrorLink } from '../DiscoverErrorLink'; describe('DiscoverErrorLink without kuery', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx index 759caa785c1af..ea79fe12ff0bd 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx @@ -6,9 +6,9 @@ import { Location } from 'history'; import React from 'react'; -import { APMError } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; -import { Span } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; -import { Transaction } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; +import { Span } from '../../../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; import { getRenderedHref } from '../../../../../utils/testHelpers'; import { DiscoverErrorLink } from '../DiscoverErrorLink'; import { DiscoverSpanLink } from '../DiscoverSpanLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx similarity index 90% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx index d72925c1956a4..5769ca34a9a87 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx @@ -6,7 +6,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { Transaction } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; import { DiscoverTransactionLink, getDiscoverQuery diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx similarity index 88% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx index 15a92474fcc6d..2f65cf7734631 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; // @ts-ignore import configureStore from '../../../../../store/config/configureStore'; import { getDiscoverQuery } from '../DiscoverTransactionLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionButton.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionButton.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionButton.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionButton.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mockTransaction.json b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mockTransaction.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mockTransaction.json rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mockTransaction.json diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/InfraLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx index 7efe5cb96cfbd..0ae9f64dc24ef 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx @@ -10,7 +10,7 @@ import url from 'url'; import { fromQuery } from './url_helpers'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { AppMountContextBasePath } from '../../../context/ApmPluginContext'; -import { InfraAppId } from '../../../../../../../plugins/infra/public'; +import { InfraAppId } from '../../../../../infra/public'; interface InfraQueryParams { time?: number; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx similarity index 88% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx index ecf788ddd2e69..81c5d17d491c0 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { getMlJobId } from '../../../../../../../../plugins/apm/common/ml_job_constants'; +import { getMlJobId } from '../../../../../common/ml_job_constants'; import { MLLink } from './MLLink'; interface Props { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx index fcc0dc7d26695..ebcf220994cda 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; interface Props extends APMLinkExtendProps { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts b/x-pack/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts rename to x-pack/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts b/x-pack/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts rename to x-pack/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx index 7d21e1efa44f2..bd3e3b36a8601 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx index 527c3da9e7e1c..1473221cca2be 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx index db1b6ec117bf4..b479ab77e1127 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx index 101f1602506aa..577209a26e46b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; const ServiceOverviewLink = (props: APMLinkExtendProps) => { const { urlParams } = useUrlParams(); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx index 371544c142a2d..dc4519365cbc2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; const TraceOverviewLink = (props: APMLinkExtendProps) => { const { urlParams } = useUrlParams(); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx index 784f9b36ff621..6278336751851 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx index af60a0a748445..ccef83ee73fb8 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx similarity index 87% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx index 0c747e0773a69..6885e44f1ad1f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx @@ -5,7 +5,7 @@ */ import { getAPMHref } from './APMLink'; -import { AgentConfigurationIntake } from '../../../../../../../../plugins/apm/common/agent_configuration/configuration_types'; +import { AgentConfigurationIntake } from '../../../../../common/agent_configuration/configuration_types'; import { history } from '../../../../utils/history'; export function editAgentConfigurationHref( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/rison_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/rison_helpers.ts rename to x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/url_helpers.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts similarity index 87% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts rename to x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts index c7d71d0b6dac5..b296302c47edf 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -6,8 +6,8 @@ import { parse, stringify } from 'query-string'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFilterName } from '../../../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters/config'; -import { url } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; +import { url } from '../../../../../../../src/plugins/kibana_utils/public'; export function toQuery(search?: string): APMQueryParamsRaw { return search ? parse(search.slice(1), { sort: false }) : {}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LoadingStatePrompt.tsx b/x-pack/plugins/apm/public/components/shared/LoadingStatePrompt.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LoadingStatePrompt.tsx rename to x-pack/plugins/apm/public/components/shared/LoadingStatePrompt.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx rename to x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx rename to x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx rename to x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx rename to x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx similarity index 91% rename from x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx rename to x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx index cede3e394cfab..2c755009ed13a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx @@ -14,10 +14,10 @@ import { import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFilterName } from '../../../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters/config'; +import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; import { Filter } from './Filter'; import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; +import { PROJECTION } from '../../../../common/projections/typings'; interface Props { projection: PROJECTION; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js b/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js rename to x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap b/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap rename to x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/index.tsx b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/index.tsx rename to x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx index 258788252379a..1913cf79c7935 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { ErrorMetadata } from '..'; import { render } from '@testing-library/react'; -import { APMError } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; import { expectTextsInDocument, expectTextsNotInDocument diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx similarity index 87% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx index ce991d8b0dc00..7cae42a94322b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx @@ -6,7 +6,7 @@ import React, { useMemo } from 'react'; import { ERROR_METADATA_SECTIONS } from './sections'; -import { APMError } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import { getSectionsWithRows } from '../helper'; import { MetadataTable } from '..'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts rename to x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/Section.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/Section.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx index 0059b7b8fb4b3..a46539fe72fcb 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { SpanMetadata } from '..'; -import { Span } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; +import { Span } from '../../../../../../typings/es_schemas/ui/span'; import { expectTextsInDocument, expectTextsNotInDocument diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx similarity index 88% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx index 2134f12531a7a..abef083e39b9e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx @@ -6,7 +6,7 @@ import React, { useMemo } from 'react'; import { SPAN_METADATA_SECTIONS } from './sections'; -import { Span } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; import { getSectionsWithRows } from '../helper'; import { MetadataTable } from '..'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts rename to x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx index 3d78f36db9786..8ae46d359efc3 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { TransactionMetadata } from '..'; import { render } from '@testing-library/react'; -import { Transaction } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; import { expectTextsInDocument, expectTextsNotInDocument diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx similarity index 87% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx index 6f93de4e87e49..86ecbba6a0aaa 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx @@ -6,7 +6,7 @@ import React, { useMemo } from 'react'; import { TRANSACTION_METADATA_SECTIONS } from './sections'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { getSectionsWithRows } from '../helper'; import { MetadataTable } from '..'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts rename to x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts rename to x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts index b65b52bf30a5c..e754f7163ca11 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts @@ -6,7 +6,7 @@ import { getSectionsWithRows, filterSectionsByTerm } from '../helper'; import { LABELS, HTTP, SERVICE } from '../sections'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; describe('MetadataTable Helper', () => { const sections = [ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/helper.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts similarity index 85% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/helper.ts rename to x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts index ef329abafa61b..a8678ee596e43 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/helper.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts @@ -6,9 +6,9 @@ import { get, pick, isEmpty } from 'lodash'; import { Section } from './sections'; -import { Transaction } from '../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; -import { APMError } from '../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; -import { Span } from '../../../../../../../plugins/apm/typings/es_schemas/ui/span'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; +import { APMError } from '../../../../typings/es_schemas/ui/apm_error'; +import { Span } from '../../../../typings/es_schemas/ui/span'; import { flattenObject, KeyValuePair } from '../../../utils/flattenObject'; export type SectionsWithRows = ReturnType; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/index.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts rename to x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx b/x-pack/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx rename to x-pack/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx b/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx rename to x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.test.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.test.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx index eab414ad47c2c..6dfc8778fe1fc 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiTitle } from '@elastic/eui'; import { px, unit } from '../../../style/variables'; import { Stacktrace } from '.'; -import { IStackframe } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; // @ts-ignore Styled Components has trouble inferring the types of the default props here. const Accordion = styled(EuiAccordion)` diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Context.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Context.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx index d289539ca44b1..d48f4b4f51a6a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Context.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx @@ -22,7 +22,7 @@ import { registerLanguage } from 'react-syntax-highlighter/dist/light'; // @ts-ignore import { xcode } from 'react-syntax-highlighter/dist/styles'; import styled from 'styled-components'; -import { IStackframeWithLineContext } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { borderRadius, px, unit, units } from '../../../style/variables'; registerLanguage('javascript', javascript); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx index daa722255bdf3..4467fe7ad615e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx @@ -7,7 +7,7 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import React, { Fragment } from 'react'; import styled from 'styled-components'; -import { IStackframe } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { fontFamilyCode, fontSize, px, units } from '../../../style/variables'; const FileDetails = styled.div` diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.test.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.test.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx index be6595153aa77..009e97358428c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx @@ -8,7 +8,7 @@ import { EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; -import { IStackframe } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { Stackframe } from './Stackframe'; import { px, unit } from '../../../style/variables'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx index 404d474a7960a..4c55add56bc40 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx @@ -11,7 +11,7 @@ import { EuiAccordion } from '@elastic/eui'; import { IStackframe, IStackframeWithLineContext -} from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +} from '../../../../typings/es_schemas/raw/fields/stackframe'; import { borderRadius, fontFamilyCode, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Variables.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Variables.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx index 0786116a659c7..ec5fb39f83f8c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Variables.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx @@ -10,7 +10,7 @@ import { EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { borderRadius, px, unit, units } from '../../../style/variables'; -import { IStackframe } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { KeyValueTable } from '../KeyValueTable'; import { flattenObject } from '../../../utils/flattenObject'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx index 1b2268326e6be..478f9cfe921d9 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx @@ -6,7 +6,7 @@ import { mount, ReactWrapper, shallow } from 'enzyme'; import React from 'react'; -import { IStackframe } from '../../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframe } from '../../../../../typings/es_schemas/raw/fields/stackframe'; import { Stackframe } from '../Stackframe'; import stacktracesMock from './stacktraces.json'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/index.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/index.test.ts.snap rename to x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/index.test.ts.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts rename to x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts index 9b6d74033e1c5..22357b9590887 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IStackframe } from '../../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframe } from '../../../../../typings/es_schemas/raw/fields/stackframe'; import { getGroupedStackframes } from '../index'; import stacktracesMock from './stacktraces.json'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/stacktraces.json b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/stacktraces.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/stacktraces.json rename to x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/stacktraces.json diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/index.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/index.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx index 141ed544a6166..b6435f7c42183 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx @@ -8,7 +8,7 @@ import { EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isEmpty, last } from 'lodash'; import React, { Fragment } from 'react'; -import { IStackframe } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { EmptyMessage } from '../../shared/EmptyMessage'; import { LibraryStacktrace } from './LibraryStacktrace'; import { Stackframe } from './Stackframe'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js b/x-pack/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js rename to x-pack/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js index 08283dee3825d..b6acb6904f865 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js +++ b/x-pack/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js @@ -7,10 +7,7 @@ import React from 'react'; import { StickyProperties } from './index'; import { shallow } from 'enzyme'; -import { - USER_ID, - URL_FULL -} from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +import { USER_ID, URL_FULL } from '../../../../common/elasticsearch_fieldnames'; import { mockMoment } from '../../../utils/testHelpers'; describe('StickyProperties', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap b/x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap rename to x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/index.tsx b/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/index.tsx rename to x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/statusCodes.ts b/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/statusCodes.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/statusCodes.ts rename to x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/statusCodes.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionResultSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/TransactionResultSummaryItem.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionResultSummaryItem.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/TransactionResultSummaryItem.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx b/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx similarity index 91% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx index f0fe57e46f2fe..f24a806426510 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { Transaction } from '../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Summary } from './'; import { TimestampTooltip } from '../TimestampTooltip'; import { DurationSummaryItem } from './DurationSummaryItem'; import { ErrorCountSummaryItemBadge } from './ErrorCountSummaryItemBadge'; -import { isRumAgentName } from '../../../../../../../plugins/apm/common/agent_name'; +import { isRumAgentName } from '../../../../common/agent_name'; import { HttpInfoSummaryItem } from './HttpInfoSummaryItem'; import { TransactionResultSummaryItem } from './TransactionResultSummaryItem'; import { UserAgentSummaryItem } from './UserAgentSummaryItem'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx similarity index 90% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx index 10a6bcc1ef7bd..8173170b72f23 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx @@ -9,7 +9,7 @@ import styled from 'styled-components'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { UserAgent } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/user_agent'; +import { UserAgent } from '../../../../typings/es_schemas/raw/fields/user_agent'; type UserAgentSummaryItemProps = UserAgent; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts b/x-pack/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts rename to x-pack/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts index 05fb73a9e2749..e1615934cd92e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts +++ b/x-pack/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; export const httpOk: Transaction = { '@timestamp': '0', diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/index.tsx index ef99f3a4933a7..ce6935d1858aa 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx @@ -8,7 +8,7 @@ import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { px, units } from '../../../../public/style/variables'; -import { Maybe } from '../../../../../../../plugins/apm/typings/common'; +import { Maybe } from '../../../../typings/common'; interface Props { items: Array>; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx b/x-pack/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx b/x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx rename to x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx similarity index 91% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx index 8df6d952cfacd..49f8fddb303bd 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx @@ -5,10 +5,10 @@ */ import React from 'react'; import { render, act, fireEvent } from '@testing-library/react'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { CustomLinkPopover } from './CustomLinkPopover'; import { expectTextsInDocument } from '../../../../utils/testHelpers'; -import { CustomLink } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; describe('CustomLinkPopover', () => { const customLinks = [ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx similarity index 90% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx index 3aed1b7ac2953..a63c226a5c46e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx @@ -12,8 +12,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; -import { CustomLink } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { CustomLinkSection } from './CustomLinkSection'; import { ManageCustomLink } from './ManageCustomLink'; import { px } from '../../../../style/variables'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx similarity index 85% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx index d429fa56894eb..6cf8b9ee5e98a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx @@ -10,8 +10,8 @@ import { expectTextsInDocument, expectTextsNotInDocument } from '../../../../utils/testHelpers'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; -import { CustomLink } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; describe('CustomLinkSection', () => { const customLinks = [ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx similarity index 86% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx index bd00bcf600ffe..e22f4b4a37745 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx @@ -7,8 +7,8 @@ import { EuiLink, EuiText } from '@elastic/eui'; import Mustache from 'mustache'; import React from 'react'; import styled from 'styled-components'; -import { CustomLink } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { px, truncate, units } from '../../../../style/variables'; const LinkContainer = styled.li` diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx index 9d1eeb9a3136d..c7a2d77d85fa6 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { render, act, fireEvent } from '@testing-library/react'; import { CustomLink } from '.'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { expectTextsInDocument, expectTextsNotInDocument } from '../../../../utils/testHelpers'; -import { CustomLink as CustomLinkType } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types'; describe('Custom links', () => { it('shows empty message when no custom link is available', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx index 38b672a181fce..710b2175e3377 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx @@ -15,12 +15,12 @@ import { import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { isEmpty } from 'lodash'; -import { CustomLink as CustomLinkType } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { ActionMenuDivider, SectionSubtitle -} from '../../../../../../../../plugins/observability/public'; +} from '../../../../../../observability/public'; import { CustomLinkSection } from './CustomLinkSection'; import { ManageCustomLink } from './ManageCustomLink'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index 7ebfe26b83630..c9376cdc01b5b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -7,8 +7,8 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { FunctionComponent, useMemo, useState } from 'react'; -import { Filter } from '../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Filter } from '../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { ActionMenu, ActionMenuDivider, @@ -17,7 +17,7 @@ import { SectionLinks, SectionSubtitle, SectionTitle -} from '../../../../../../../plugins/observability/public'; +} from '../../../../../observability/public'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useFetcher } from '../../../hooks/useFetcher'; import { useLocation } from '../../../hooks/useLocation'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index 8dc2076eab5b5..cda602204469c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { render, fireEvent, act, wait } from '@testing-library/react'; import { TransactionActionMenu } from '../TransactionActionMenu'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import * as Transactions from './mockData'; import { expectTextsNotInDocument, @@ -15,7 +15,7 @@ import { } from '../../../../utils/testHelpers'; import * as hooks from '../../../../hooks/useFetcher'; import { LicenseContext } from '../../../../context/LicenseContext'; -import { License } from '../../../../../../../../plugins/licensing/common/license'; +import { License } from '../../../../../../licensing/common/license'; import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; import * as apmApi from '../../../../services/rest/createCallApmApi'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts index 3032dd1704f4e..b2f6f39e0b596 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts @@ -5,7 +5,7 @@ */ import { Location } from 'history'; import { getSections } from '../sections'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { AppMountContextBasePath } from '../../../../context/ApmPluginContext'; describe('Transaction action menu', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts index ffdf0b485da64..2c2f4bfcadd7d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts @@ -8,7 +8,7 @@ import { Location } from 'history'; import { pick, isEmpty } from 'lodash'; import moment from 'moment'; import url from 'url'; -import { Transaction } from '../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; import { getDiscoverHref } from '../Links/DiscoverLinks/DiscoverLink'; import { getDiscoverQuery } from '../Links/DiscoverLinks/DiscoverTransactionLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx similarity index 81% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx index 50ea169c017f9..966cc64fde505 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -7,17 +7,14 @@ import React, { useMemo } from 'react'; import numeral from '@elastic/numeral'; import { throttle } from 'lodash'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../../plugins/apm/common/i18n'; -import { - Coordinate, - TimeSeries -} from '../../../../../../../../plugins/apm/typings/timeseries'; -import { Maybe } from '../../../../../../../../plugins/apm/typings/common'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; +import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; +import { Maybe } from '../../../../../typings/common'; import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart'; import { asPercent } from '../../../../utils/formatters'; import { unit } from '../../../../style/variables'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { useUiTracker } from '../../../../../../../../plugins/observability/public'; +import { useUiTracker } from '../../../../../../observability/public'; interface Props { timeseries: TimeSeries[]; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx index 91f5f4e0a7176..c4a8e07fb3004 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx @@ -13,10 +13,7 @@ import { EuiIcon } from '@elastic/eui'; import styled from 'styled-components'; -import { - FORMATTERS, - InfraFormatterType -} from '../../../../../../../plugins/infra/public'; +import { FORMATTERS, InfraFormatterType } from '../../../../../infra/public'; interface TransactionBreakdownKpi { name: string; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx index 85f5f83fb920e..be5860190c11e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx @@ -11,7 +11,7 @@ import { TransactionBreakdownHeader } from './TransactionBreakdownHeader'; import { TransactionBreakdownKpiList } from './TransactionBreakdownKpiList'; import { TransactionBreakdownGraph } from './TransactionBreakdownGraph'; import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { useUiTracker } from '../../../../../../../plugins/observability/public'; +import { useUiTracker } from '../../../../../observability/public'; const emptyMessage = i18n.translate('xpack.apm.transactionBreakdown.noData', { defaultMessage: 'No data within this time range.' diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx index 077e6535a8b21..1e9fbd2c1c135 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { map } from 'lodash'; import { EuiFieldNumber, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ForLastExpression } from '../../../../../../../plugins/triggers_actions_ui/public'; +import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { TRANSACTION_ALERT_AGGREGATION_TYPES, ALERT_TYPES_CONFIG -} from '../../../../../../../plugins/apm/common/alert_types'; +} from '../../../../common/alert_types'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx index ec6168df5b134..6eff4759b2e7c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx @@ -14,8 +14,8 @@ import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Maybe } from '../../../../../../../../plugins/apm/typings/common'; -import { Annotation } from '../../../../../../../../plugins/apm/common/annotations'; +import { Maybe } from '../../../../../typings/common'; +import { Annotation } from '../../../../../common/annotations'; import { PlotValues, SharedPlot } from './plotUtils'; import { asAbsoluteDateTime } from '../../../../utils/formatters'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/SelectionMarker.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/SelectionMarker.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/SelectionMarker.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/SelectionMarker.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StatusText.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StatusText.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StatusText.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StatusText.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getEmptySeries.ts b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getEmptySeries.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getEmptySeries.ts rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getEmptySeries.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/index.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts index bfc5c7c243f31..b130deed7f098 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts @@ -6,10 +6,7 @@ // @ts-ignore import * as plotUtils from './plotUtils'; -import { - TimeSeries, - Coordinate -} from '../../../../../../../../plugins/apm/typings/timeseries'; +import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; describe('plotUtils', () => { describe('getPlotValues', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx index c489c270d19ac..64350a5741647 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx @@ -11,10 +11,7 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import React from 'react'; -import { - TimeSeries, - Coordinate -} from '../../../../../../../../plugins/apm/typings/timeseries'; +import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; import { unit } from '../../../../style/variables'; import { getDomainTZ, getTimeTicksTZ } from '../helper/timezone'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js rename to x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js rename to x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap rename to x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json rename to x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js rename to x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx similarity index 90% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx index 2ceac87d9aab3..862f2a8987067 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx @@ -6,7 +6,7 @@ import { EuiTitle } from '@elastic/eui'; import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GenericMetricsChart } from '../../../../../../../../plugins/apm/server/lib/metrics/transform_metrics_chart'; +import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart'; // @ts-ignore import CustomPlot from '../CustomPlot'; import { @@ -17,10 +17,10 @@ import { getFixedByteFormatter, asDuration } from '../../../../utils/formatters'; -import { Coordinate } from '../../../../../../../../plugins/apm/typings/timeseries'; +import { Coordinate } from '../../../../../typings/timeseries'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { useChartsSync } from '../../../../hooks/useChartsSync'; -import { Maybe } from '../../../../../../../../plugins/apm/typings/common'; +import { Maybe } from '../../../../../typings/common'; interface Props { start: Maybe; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/LastTickValue.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/LastTickValue.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/LastTickValue.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/LastTickValue.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx index 48265ce7c80a8..51368a4fb946d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { TRACE_ID, TRANSACTION_ID -} from '../../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../../../../common/elasticsearch_fieldnames'; import { useUrlParams } from '../../../../../hooks/useUrlParams'; import { px, unit, units } from '../../../../../style/variables'; import { asDuration } from '../../../../../utils/formatters'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/AgentMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/AgentMarker.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/AgentMarker.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/AgentMarker.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/ErrorMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/ErrorMarker.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/ErrorMarker.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/ErrorMarker.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/Marker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/Marker.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/Marker.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/Marker.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/AgentMarker.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/AgentMarker.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/AgentMarker.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/AgentMarker.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/ErrorMarker.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/ErrorMarker.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/ErrorMarker.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/ErrorMarker.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/Marker.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/Marker.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/Marker.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/Marker.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/plotUtils.ts b/x-pack/plugins/apm/public/components/shared/charts/Timeline/plotUtils.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/plotUtils.ts rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/plotUtils.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js b/x-pack/plugins/apm/public/components/shared/charts/Tooltip/index.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js rename to x-pack/plugins/apm/public/components/shared/charts/Tooltip/index.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx rename to x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx rename to x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx index c9c31b05e264c..27c829f63cf0a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx @@ -8,7 +8,7 @@ import React, { useCallback } from 'react'; import { Coordinate, RectCoordinate -} from '../../../../../../../../../plugins/apm/typings/timeseries'; +} from '../../../../../../typings/timeseries'; import { useChartsSync } from '../../../../../hooks/useChartsSync'; // @ts-ignore import CustomPlot from '../../CustomPlot'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 368a39e4ad228..b0555da705a30 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -19,11 +19,8 @@ import { Location } from 'history'; import React, { Component } from 'react'; import { isEmpty, flatten } from 'lodash'; import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../../plugins/apm/common/i18n'; -import { - Coordinate, - TimeSeries -} from '../../../../../../../../plugins/apm/typings/timeseries'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; +import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; import { ITransactionChartData } from '../../../../selectors/chartSelectors'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { @@ -42,7 +39,7 @@ import { TRANSACTION_PAGE_LOAD, TRANSACTION_ROUTE_CHANGE, TRANSACTION_REQUEST -} from '../../../../../../../../plugins/apm/common/transaction_types'; +} from '../../../../../common/transaction_types'; interface TransactionChartProps { hasMLJob: boolean; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts rename to x-pack/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/helper/timezone.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/timezone.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/helper/timezone.ts rename to x-pack/plugins/apm/public/components/shared/charts/helper/timezone.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.test.tsx b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts rename to x-pack/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.ts b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.ts rename to x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.ts diff --git a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx rename to x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx index cc2e382611628..865e3dbe6dafc 100644 --- a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx +++ b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { ApmPluginContext, ApmPluginContextValue } from '.'; import { createCallApmApi } from '../../services/rest/createCallApmApi'; -import { ConfigSchema } from '../../new-platform/plugin'; +import { ConfigSchema } from '../..'; const mockCore = { chrome: { @@ -30,7 +30,6 @@ const mockCore = { }; const mockConfig: ConfigSchema = { - indexPatternTitle: 'apm-*', serviceMapEnabled: true, ui: { enabled: false diff --git a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/index.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx similarity index 87% rename from x-pack/legacy/plugins/apm/public/context/ApmPluginContext/index.tsx rename to x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx index acc3886586889..37304d292540d 100644 --- a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/index.tsx +++ b/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx @@ -6,7 +6,8 @@ import { createContext } from 'react'; import { AppMountContext } from 'kibana/public'; -import { ApmPluginSetupDeps, ConfigSchema } from '../../new-platform/plugin'; +import { ConfigSchema } from '../..'; +import { ApmPluginSetupDeps } from '../../plugin'; export type AppMountContextBasePath = AppMountContext['core']['http']['basePath']; diff --git a/x-pack/legacy/plugins/apm/public/context/ChartsSyncContext.tsx b/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/ChartsSyncContext.tsx rename to x-pack/plugins/apm/public/context/ChartsSyncContext.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx b/x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx rename to x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx b/x-pack/plugins/apm/public/context/LicenseContext/index.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx rename to x-pack/plugins/apm/public/context/LicenseContext/index.tsx index 62cdbd3bbc995..e6615a2fc98bf 100644 --- a/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx +++ b/x-pack/plugins/apm/public/context/LicenseContext/index.tsx @@ -6,7 +6,7 @@ import React from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { ILicense } from '../../../../../../plugins/licensing/public'; +import { ILicense } from '../../../../licensing/public'; import { useApmPluginContext } from '../../hooks/useApmPluginContext'; import { InvalidLicenseNotification } from './InvalidLicenseNotification'; diff --git a/x-pack/legacy/plugins/apm/public/context/LoadingIndicatorContext.tsx b/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/LoadingIndicatorContext.tsx rename to x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/LocationContext.tsx b/x-pack/plugins/apm/public/context/LocationContext.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/LocationContext.tsx rename to x-pack/plugins/apm/public/context/LocationContext.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/MatchedRouteContext.tsx b/x-pack/plugins/apm/public/context/MatchedRouteContext.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/MatchedRouteContext.tsx rename to x-pack/plugins/apm/public/context/MatchedRouteContext.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx b/x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx rename to x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx b/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx rename to x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/constants.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/constants.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/UrlParamsContext/constants.ts rename to x-pack/plugins/apm/public/context/UrlParamsContext/constants.ts diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts similarity index 97% rename from x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts rename to x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts index b80db0e9ae073..f1e45fe45255d 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts @@ -7,7 +7,7 @@ import { compact, pick } from 'lodash'; import datemath from '@elastic/datemath'; import { IUrlParams } from './types'; -import { ProcessorEvent } from '../../../../../../plugins/apm/common/processor_event'; +import { ProcessorEvent } from '../../../common/processor_event'; interface PathParams { processorEvent?: ProcessorEvent; diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx b/x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx rename to x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx index 588936039c2bc..7a929380bce37 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx @@ -16,13 +16,13 @@ import { uniqueId, mapValues } from 'lodash'; import { IUrlParams } from './types'; import { getParsedDate } from './helpers'; import { resolveUrlParams } from './resolveUrlParams'; -import { UIFilters } from '../../../../../../plugins/apm/typings/ui_filters'; +import { UIFilters } from '../../../typings/ui_filters'; import { localUIFilterNames, LocalUIFilterName // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters/config'; -import { pickKeys } from '../../utils/pickKeys'; +} from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { pickKeys } from '../../../common/utils/pick_keys'; import { useDeepObjectIdentity } from '../../hooks/useDeepObjectIdentity'; interface TimeRange { diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts similarity index 93% rename from x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts rename to x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts index f022d2084583b..34af18431a2df 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts @@ -18,8 +18,8 @@ import { import { toQuery } from '../../components/shared/Links/url_helpers'; import { TIMEPICKER_DEFAULTS } from './constants'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { localUIFilterNames } from '../../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters/config'; -import { pickKeys } from '../../utils/pickKeys'; +import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { pickKeys } from '../../../common/utils/pick_keys'; type TimeUrlParams = Pick< IUrlParams, diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts similarity index 83% rename from x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts rename to x-pack/plugins/apm/public/context/UrlParamsContext/types.ts index acde09308ab46..78fe662b88d75 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts @@ -5,8 +5,8 @@ */ // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFilterName } from '../../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters/config'; -import { ProcessorEvent } from '../../../../../../plugins/apm/common/processor_event'; +import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { ProcessorEvent } from '../../../common/processor_event'; export type IUrlParams = { detailTab?: string; diff --git a/x-pack/legacy/plugins/apm/public/new-platform/featureCatalogueEntry.ts b/x-pack/plugins/apm/public/featureCatalogueEntry.ts similarity index 88% rename from x-pack/legacy/plugins/apm/public/new-platform/featureCatalogueEntry.ts rename to x-pack/plugins/apm/public/featureCatalogueEntry.ts index 7a150de6d5d02..f76c6f5169dc5 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/featureCatalogueEntry.ts +++ b/x-pack/plugins/apm/public/featureCatalogueEntry.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { FeatureCatalogueCategory } from '../../../../../../src/plugins/home/public'; +import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; export const featureCatalogueEntry = { id: 'apm', diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAgentName.ts b/x-pack/plugins/apm/public/hooks/useAgentName.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useAgentName.ts rename to x-pack/plugins/apm/public/hooks/useAgentName.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useApmPluginContext.ts b/x-pack/plugins/apm/public/hooks/useApmPluginContext.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useApmPluginContext.ts rename to x-pack/plugins/apm/public/hooks/useApmPluginContext.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts rename to x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts similarity index 83% rename from x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts rename to x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts index 256c2fa68bfbc..5d0c9d1435798 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts +++ b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts @@ -8,9 +8,9 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { useFetcher } from './useFetcher'; import { useUrlParams } from './useUrlParams'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AvgDurationByBrowserAPIResponse } from '../../../../../plugins/apm/server/lib/transactions/avg_duration_by_browser'; -import { TimeSeries } from '../../../../../plugins/apm/typings/timeseries'; -import { getVizColorForIndex } from '../../../../../plugins/apm/common/viz_colors'; +import { AvgDurationByBrowserAPIResponse } from '../../server/lib/transactions/avg_duration_by_browser'; +import { TimeSeries } from '../../typings/timeseries'; +import { getVizColorForIndex } from '../../common/viz_colors'; function toTimeSeries(data?: AvgDurationByBrowserAPIResponse): TimeSeries[] { if (!data) { diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByCountry.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByCountry.ts rename to x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useCallApi.ts b/x-pack/plugins/apm/public/hooks/useCallApi.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useCallApi.ts rename to x-pack/plugins/apm/public/hooks/useCallApi.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useChartsSync.tsx b/x-pack/plugins/apm/public/hooks/useChartsSync.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useChartsSync.tsx rename to x-pack/plugins/apm/public/hooks/useChartsSync.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useComponentId.tsx b/x-pack/plugins/apm/public/hooks/useComponentId.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useComponentId.tsx rename to x-pack/plugins/apm/public/hooks/useComponentId.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useDeepObjectIdentity.ts b/x-pack/plugins/apm/public/hooks/useDeepObjectIdentity.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useDeepObjectIdentity.ts rename to x-pack/plugins/apm/public/hooks/useDeepObjectIdentity.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useDynamicIndexPattern.ts b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts similarity index 89% rename from x-pack/legacy/plugins/apm/public/hooks/useDynamicIndexPattern.ts rename to x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts index ee3d2e81f259f..9a95bd925d6e1 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useDynamicIndexPattern.ts +++ b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts @@ -5,7 +5,7 @@ */ import { useFetcher } from './useFetcher'; -import { ProcessorEvent } from '../../../../../plugins/apm/common/processor_event'; +import { ProcessorEvent } from '../../common/processor_event'; export function useDynamicIndexPattern( processorEvent: ProcessorEvent | undefined diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx b/x-pack/plugins/apm/public/hooks/useFetcher.integration.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx rename to x-pack/plugins/apm/public/hooks/useFetcher.integration.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx b/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx rename to x-pack/plugins/apm/public/hooks/useFetcher.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/plugins/apm/public/hooks/useFetcher.tsx similarity index 98% rename from x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx rename to x-pack/plugins/apm/public/hooks/useFetcher.tsx index 95cebd6b2a465..5d5128d969aad 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/useFetcher.tsx @@ -9,7 +9,7 @@ import React, { useContext, useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { IHttpFetchError } from 'src/core/public'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { LoadingIndicatorContext } from '../context/LoadingIndicatorContext'; import { APMClient, callApmApi } from '../services/rest/createCallApmApi'; import { useApmPluginContext } from './useApmPluginContext'; diff --git a/x-pack/legacy/plugins/apm/public/hooks/useKibanaUrl.ts b/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useKibanaUrl.ts rename to x-pack/plugins/apm/public/hooks/useKibanaUrl.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useLicense.ts b/x-pack/plugins/apm/public/hooks/useLicense.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useLicense.ts rename to x-pack/plugins/apm/public/hooks/useLicense.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useLoadingIndicator.ts b/x-pack/plugins/apm/public/hooks/useLoadingIndicator.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useLoadingIndicator.ts rename to x-pack/plugins/apm/public/hooks/useLoadingIndicator.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts similarity index 88% rename from x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts rename to x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index 9f14b2b25fc94..1dfd3ec7c3ee3 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -7,18 +7,18 @@ import { omit } from 'lodash'; import { useFetcher } from './useFetcher'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFiltersAPIResponse } from '../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters'; +import { LocalUIFiltersAPIResponse } from '../../server/lib/ui_filters/local_ui_filters'; import { useUrlParams } from './useUrlParams'; import { LocalUIFilterName, localUIFilters // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters/config'; +} from '../../server/lib/ui_filters/local_ui_filters/config'; import { history } from '../utils/history'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; -import { PROJECTION } from '../../../../../plugins/apm/common/projections/typings'; -import { pickKeys } from '../utils/pickKeys'; +import { PROJECTION } from '../../common/projections/typings'; +import { pickKeys } from '../../common/utils/pick_keys'; import { useCallApi } from './useCallApi'; const getInitialData = ( diff --git a/x-pack/legacy/plugins/apm/public/hooks/useLocation.tsx b/x-pack/plugins/apm/public/hooks/useLocation.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useLocation.tsx rename to x-pack/plugins/apm/public/hooks/useLocation.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useMatchedRoutes.tsx b/x-pack/plugins/apm/public/hooks/useMatchedRoutes.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useMatchedRoutes.tsx rename to x-pack/plugins/apm/public/hooks/useMatchedRoutes.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts b/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts similarity index 91% rename from x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts rename to x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts index 72618a6254f4c..ebcd6ab063708 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts +++ b/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts @@ -5,7 +5,7 @@ */ // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetricsChartsByAgentAPIResponse } from '../../../../../plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent'; +import { MetricsChartsByAgentAPIResponse } from '../../server/lib/metrics/get_metrics_chart_data_by_agent'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useUiFilters } from '../context/UrlParamsContext'; import { useFetcher } from './useFetcher'; diff --git a/x-pack/legacy/plugins/apm/public/hooks/useServiceTransactionTypes.tsx b/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useServiceTransactionTypes.tsx rename to x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useTransactionBreakdown.ts rename to x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionCharts.ts b/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useTransactionCharts.ts rename to x-pack/plugins/apm/public/hooks/useTransactionCharts.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts similarity index 93% rename from x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts rename to x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts index 9a93a2334924a..152980b5655d6 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -8,7 +8,7 @@ import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; import { useUiFilters } from '../context/UrlParamsContext'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionDistributionAPIResponse } from '../../../../../plugins/apm/server/lib/transactions/distribution'; +import { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution'; const INITIAL_DATA = { buckets: [] as TransactionDistributionAPIResponse['buckets'], diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts similarity index 94% rename from x-pack/legacy/plugins/apm/public/hooks/useTransactionList.ts rename to x-pack/plugins/apm/public/hooks/useTransactionList.ts index 6ede77023790b..e048e8fe0e3cb 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useTransactionList.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts @@ -9,7 +9,7 @@ import { IUrlParams } from '../context/UrlParamsContext/types'; import { useUiFilters } from '../context/UrlParamsContext'; import { useFetcher } from './useFetcher'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionGroupListAPIResponse } from '../../../../../plugins/apm/server/lib/transaction_groups'; +import { TransactionGroupListAPIResponse } from '../../server/lib/transaction_groups'; const getRelativeImpact = ( impact: number, diff --git a/x-pack/legacy/plugins/apm/public/hooks/useUrlParams.tsx b/x-pack/plugins/apm/public/hooks/useUrlParams.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useUrlParams.tsx rename to x-pack/plugins/apm/public/hooks/useUrlParams.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts b/x-pack/plugins/apm/public/hooks/useWaterfall.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts rename to x-pack/plugins/apm/public/hooks/useWaterfall.ts diff --git a/x-pack/legacy/plugins/apm/public/icon.svg b/x-pack/plugins/apm/public/icon.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/icon.svg rename to x-pack/plugins/apm/public/icon.svg diff --git a/x-pack/legacy/plugins/apm/public/images/apm-ml-anomaly-detection-example.png b/x-pack/plugins/apm/public/images/apm-ml-anomaly-detection-example.png similarity index 100% rename from x-pack/legacy/plugins/apm/public/images/apm-ml-anomaly-detection-example.png rename to x-pack/plugins/apm/public/images/apm-ml-anomaly-detection-example.png diff --git a/x-pack/plugins/apm/public/index.ts b/x-pack/plugins/apm/public/index.ts new file mode 100644 index 0000000000000..4ac06e1eb8a1c --- /dev/null +++ b/x-pack/plugins/apm/public/index.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 { + PluginInitializer, + PluginInitializerContext +} from '../../../../src/core/public'; +import { ApmPlugin, ApmPluginSetup, ApmPluginStart } from './plugin'; + +export interface ConfigSchema { + serviceMapEnabled: boolean; + ui: { + enabled: boolean; + }; +} + +export const plugin: PluginInitializer = ( + pluginInitializerContext: PluginInitializerContext +) => new ApmPlugin(pluginInitializerContext); + +export { ApmPluginSetup, ApmPluginStart }; +export { getTraceUrl } from './components/shared/Links/apm/ExternalLinks'; diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts new file mode 100644 index 0000000000000..f13c8853d0582 --- /dev/null +++ b/x-pack/plugins/apm/public/plugin.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext +} from '../../../../src/core/public'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; + +import { + PluginSetupContract as AlertingPluginPublicSetup, + PluginStartContract as AlertingPluginPublicStart +} from '../../alerting/public'; +import { FeaturesPluginSetup } from '../../features/public'; +import { + DataPublicPluginSetup, + DataPublicPluginStart +} from '../../../../src/plugins/data/public'; +import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { + TriggersAndActionsUIPublicPluginSetup, + TriggersAndActionsUIPublicPluginStart +} from '../../triggers_actions_ui/public'; +import { ConfigSchema } from '.'; +import { createCallApmApi } from './services/rest/createCallApmApi'; +import { featureCatalogueEntry } from './featureCatalogueEntry'; +import { AlertType } from '../common/alert_types'; +import { ErrorRateAlertTrigger } from './components/shared/ErrorRateAlertTrigger'; +import { TransactionDurationAlertTrigger } from './components/shared/TransactionDurationAlertTrigger'; +import { setHelpExtension } from './setHelpExtension'; +import { toggleAppLinkInNav } from './toggleAppLinkInNav'; +import { setReadonlyBadge } from './updateBadge'; +import { createStaticIndexPattern } from './services/rest/index_pattern'; + +export type ApmPluginSetup = void; +export type ApmPluginStart = void; + +export interface ApmPluginSetupDeps { + alerting?: AlertingPluginPublicSetup; + data: DataPublicPluginSetup; + features: FeaturesPluginSetup; + home: HomePublicPluginSetup; + licensing: LicensingPluginSetup; + triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; +} + +export interface ApmPluginStartDeps { + alerting?: AlertingPluginPublicStart; + data: DataPublicPluginStart; + home: void; + licensing: void; + triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; +} + +export class ApmPlugin implements Plugin { + private readonly initializerContext: PluginInitializerContext; + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + public setup(core: CoreSetup, plugins: ApmPluginSetupDeps) { + const config = this.initializerContext.config.get(); + const pluginSetupDeps = plugins; + + pluginSetupDeps.home.environment.update({ apmUi: true }); + pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry); + + core.application.register({ + id: 'apm', + title: 'APM', + order: 8100, + euiIconType: 'apmApp', + appRoute: '/app/apm', + icon: 'plugins/apm/public/icon.svg', + category: DEFAULT_APP_CATEGORIES.observability, + + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services + const [coreStart] = await core.getStartServices(); + + // render APM feedback link in global help menu + setHelpExtension(coreStart); + setReadonlyBadge(coreStart); + + // Automatically creates static index pattern and stores as saved object + createStaticIndexPattern().catch(e => { + // eslint-disable-next-line no-console + console.log('Error creating static index pattern', e); + }); + + return renderApp(coreStart, pluginSetupDeps, params, config); + } + }); + } + public start(core: CoreStart, plugins: ApmPluginStartDeps) { + createCallApmApi(core.http); + + toggleAppLinkInNav(core, this.initializerContext.config.get()); + + plugins.triggers_actions_ui.alertTypeRegistry.register({ + id: AlertType.ErrorRate, + name: i18n.translate('xpack.apm.alertTypes.errorRate', { + defaultMessage: 'Error rate' + }), + iconClass: 'bell', + alertParamsExpression: ErrorRateAlertTrigger, + validate: () => ({ + errors: [] + }) + }); + + plugins.triggers_actions_ui.alertTypeRegistry.register({ + id: AlertType.TransactionDuration, + name: i18n.translate('xpack.apm.alertTypes.transactionDuration', { + defaultMessage: 'Transaction duration' + }), + iconClass: 'bell', + alertParamsExpression: TransactionDurationAlertTrigger, + validate: () => ({ + errors: [] + }) + }); + } +} diff --git a/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts b/x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts rename to x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts diff --git a/x-pack/legacy/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts b/x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts rename to x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts diff --git a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/plugins/apm/public/selectors/chartSelectors.ts similarity index 94% rename from x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts rename to x-pack/plugins/apm/public/selectors/chartSelectors.ts index d60b63e243d71..e6ef9361ee52a 100644 --- a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts +++ b/x-pack/plugins/apm/public/selectors/chartSelectors.ts @@ -10,14 +10,14 @@ import { difference, zipObject } from 'lodash'; import mean from 'lodash.mean'; import { rgba } from 'polished'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TimeSeriesAPIResponse } from '../../../../../plugins/apm/server/lib/transactions/charts'; +import { TimeSeriesAPIResponse } from '../../server/lib/transactions/charts'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ApmTimeSeriesResponse } from '../../../../../plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform'; +import { ApmTimeSeriesResponse } from '../../server/lib/transactions/charts/get_timeseries_data/transform'; import { Coordinate, RectCoordinate, TimeSeries -} from '../../../../../plugins/apm/typings/timeseries'; +} from '../../typings/timeseries'; import { asDecimal, tpmUnit, convertTo } from '../utils/formatters'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { getEmptySeries } from '../components/shared/charts/CustomPlot/getEmptySeries'; diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/SessionStorageMock.ts b/x-pack/plugins/apm/public/services/__test__/SessionStorageMock.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/services/__test__/SessionStorageMock.ts rename to x-pack/plugins/apm/public/services/__test__/SessionStorageMock.ts diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts b/x-pack/plugins/apm/public/services/__test__/callApi.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts rename to x-pack/plugins/apm/public/services/__test__/callApi.test.ts diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts b/x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts rename to x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts diff --git a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts b/x-pack/plugins/apm/public/services/rest/callApi.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/services/rest/callApi.ts rename to x-pack/plugins/apm/public/services/rest/callApi.ts diff --git a/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts similarity index 89% rename from x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts rename to x-pack/plugins/apm/public/services/rest/createCallApmApi.ts index 2fffb40d353fc..1027e8b885d71 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts @@ -6,9 +6,9 @@ import { HttpSetup } from 'kibana/public'; import { callApi, FetchOptions } from './callApi'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { APMAPI } from '../../../../../../plugins/apm/server/routes/create_apm_api'; +import { APMAPI } from '../../../server/routes/create_apm_api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Client } from '../../../../../../plugins/apm/server/routes/typings'; +import { Client } from '../../../server/routes/typings'; export type APMClient = Client; export type APMClientOptions = Omit & { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts b/x-pack/plugins/apm/public/services/rest/index_pattern.ts similarity index 76% rename from x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts rename to x-pack/plugins/apm/public/services/rest/index_pattern.ts index 1efcc98bbbd66..ac7a0d3cf734b 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts +++ b/x-pack/plugins/apm/public/services/rest/index_pattern.ts @@ -12,3 +12,9 @@ export const createStaticIndexPattern = async () => { pathname: '/api/apm/index_pattern/static' }); }; + +export const getApmIndexPatternTitle = async () => { + return await callApmApi({ + pathname: '/api/apm/index_pattern/title' + }); +}; diff --git a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts b/x-pack/plugins/apm/public/services/rest/ml.ts similarity index 91% rename from x-pack/legacy/plugins/apm/public/services/rest/ml.ts rename to x-pack/plugins/apm/public/services/rest/ml.ts index 0cd1bdf907531..b333a08d2eb05 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts +++ b/x-pack/plugins/apm/public/services/rest/ml.ts @@ -9,14 +9,14 @@ import { PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE -} from '../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../common/elasticsearch_fieldnames'; import { getMlJobId, getMlPrefix, encodeForMlApi -} from '../../../../../../plugins/apm/common/ml_job_constants'; +} from '../../../common/ml_job_constants'; import { callApi } from './callApi'; -import { ESFilter } from '../../../../../../plugins/apm/typings/elasticsearch'; +import { ESFilter } from '../../../typings/elasticsearch'; import { callApmApi } from './createCallApmApi'; interface MlResponseItem { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts b/x-pack/plugins/apm/public/services/rest/watcher.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/services/rest/watcher.ts rename to x-pack/plugins/apm/public/services/rest/watcher.ts diff --git a/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts b/x-pack/plugins/apm/public/setHelpExtension.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts rename to x-pack/plugins/apm/public/setHelpExtension.ts diff --git a/x-pack/legacy/plugins/apm/public/style/variables.ts b/x-pack/plugins/apm/public/style/variables.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/style/variables.ts rename to x-pack/plugins/apm/public/style/variables.ts diff --git a/x-pack/legacy/plugins/apm/public/new-platform/toggleAppLinkInNav.ts b/x-pack/plugins/apm/public/toggleAppLinkInNav.ts similarity index 91% rename from x-pack/legacy/plugins/apm/public/new-platform/toggleAppLinkInNav.ts rename to x-pack/plugins/apm/public/toggleAppLinkInNav.ts index c807cebf97525..8204e1a022d7e 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/toggleAppLinkInNav.ts +++ b/x-pack/plugins/apm/public/toggleAppLinkInNav.ts @@ -5,7 +5,7 @@ */ import { CoreStart } from 'kibana/public'; -import { ConfigSchema } from './plugin'; +import { ConfigSchema } from '.'; export function toggleAppLinkInNav(core: CoreStart, { ui }: ConfigSchema) { if (ui.enabled === false) { diff --git a/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts b/x-pack/plugins/apm/public/updateBadge.ts similarity index 99% rename from x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts rename to x-pack/plugins/apm/public/updateBadge.ts index b3e29bb891c23..10849754313c4 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts +++ b/x-pack/plugins/apm/public/updateBadge.ts @@ -10,7 +10,6 @@ import { CoreStart } from 'kibana/public'; export function setReadonlyBadge({ application, chrome }: CoreStart) { const canSave = application.capabilities.apm.save; const { setBadge } = chrome; - setBadge( !canSave ? { diff --git a/x-pack/legacy/plugins/apm/public/utils/__test__/flattenObject.test.ts b/x-pack/plugins/apm/public/utils/__test__/flattenObject.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/__test__/flattenObject.test.ts rename to x-pack/plugins/apm/public/utils/__test__/flattenObject.test.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts b/x-pack/plugins/apm/public/utils/flattenObject.ts similarity index 94% rename from x-pack/legacy/plugins/apm/public/utils/flattenObject.ts rename to x-pack/plugins/apm/public/utils/flattenObject.ts index 020bfec2cbd6a..295ea1f9f900f 100644 --- a/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts +++ b/x-pack/plugins/apm/public/utils/flattenObject.ts @@ -5,7 +5,7 @@ */ import { compact, isObject } from 'lodash'; -import { Maybe } from '../../../../../plugins/apm/typings/common'; +import { Maybe } from '../../typings/common'; export interface KeyValuePair { key: string; diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/datetime.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts rename to x-pack/plugins/apm/public/utils/formatters/__test__/datetime.test.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts rename to x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts rename to x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/size.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts rename to x-pack/plugins/apm/public/utils/formatters/__test__/size.test.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts b/x-pack/plugins/apm/public/utils/formatters/datetime.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts rename to x-pack/plugins/apm/public/utils/formatters/datetime.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts b/x-pack/plugins/apm/public/utils/formatters/duration.ts similarity index 96% rename from x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts rename to x-pack/plugins/apm/public/utils/formatters/duration.ts index 681d876ca3beb..39341e1ff4443 100644 --- a/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts +++ b/x-pack/plugins/apm/public/utils/formatters/duration.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { memoize } from 'lodash'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../plugins/apm/common/i18n'; +import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; import { asDecimal, asInteger } from './formatters'; import { TimeUnit } from './datetime'; -import { Maybe } from '../../../../../../plugins/apm/typings/common'; +import { Maybe } from '../../../typings/common'; interface FormatterOptions { defaultValue?: string; diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts b/x-pack/plugins/apm/public/utils/formatters/formatters.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts rename to x-pack/plugins/apm/public/utils/formatters/formatters.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts b/x-pack/plugins/apm/public/utils/formatters/index.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/index.ts rename to x-pack/plugins/apm/public/utils/formatters/index.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts b/x-pack/plugins/apm/public/utils/formatters/size.ts similarity index 95% rename from x-pack/legacy/plugins/apm/public/utils/formatters/size.ts rename to x-pack/plugins/apm/public/utils/formatters/size.ts index 8fe6ebf3e573d..2cdf8af1d46de 100644 --- a/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts +++ b/x-pack/plugins/apm/public/utils/formatters/size.ts @@ -5,7 +5,7 @@ */ import { memoize } from 'lodash'; import { asDecimal } from './formatters'; -import { Maybe } from '../../../../../../plugins/apm/typings/common'; +import { Maybe } from '../../../typings/common'; function asKilobytes(value: number) { return `${asDecimal(value / 1000)} KB`; diff --git a/x-pack/legacy/plugins/apm/public/utils/getRangeFromTimeSeries.ts b/x-pack/plugins/apm/public/utils/getRangeFromTimeSeries.ts similarity index 88% rename from x-pack/legacy/plugins/apm/public/utils/getRangeFromTimeSeries.ts rename to x-pack/plugins/apm/public/utils/getRangeFromTimeSeries.ts index 4301ead2fc79f..1865d5ae574a7 100644 --- a/x-pack/legacy/plugins/apm/public/utils/getRangeFromTimeSeries.ts +++ b/x-pack/plugins/apm/public/utils/getRangeFromTimeSeries.ts @@ -5,7 +5,7 @@ */ import { flatten } from 'lodash'; -import { TimeSeries } from '../../../../../plugins/apm/typings/timeseries'; +import { TimeSeries } from '../../typings/timeseries'; export const getRangeFromTimeSeries = (timeseries: TimeSeries[]) => { const dataPoints = flatten(timeseries.map(series => series.data)); diff --git a/x-pack/legacy/plugins/apm/public/utils/history.ts b/x-pack/plugins/apm/public/utils/history.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/history.ts rename to x-pack/plugins/apm/public/utils/history.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/httpStatusCodeToColor.ts b/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/httpStatusCodeToColor.ts rename to x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts b/x-pack/plugins/apm/public/utils/isValidCoordinateValue.ts similarity index 84% rename from x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts rename to x-pack/plugins/apm/public/utils/isValidCoordinateValue.ts index f7c13603c3535..c36efc232b782 100644 --- a/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts +++ b/x-pack/plugins/apm/public/utils/isValidCoordinateValue.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 { Maybe } from '../../../../../plugins/apm/typings/common'; +import { Maybe } from '../../typings/common'; export const isValidCoordinateValue = (value: Maybe): value is number => value !== null && value !== undefined; diff --git a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx rename to x-pack/plugins/apm/public/utils/testHelpers.tsx index 36c0e18777bfd..def41a1cabd61 100644 --- a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -16,14 +16,14 @@ import { render, waitForElement } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { MemoryRouter } from 'react-router-dom'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { APMConfig } from '../../../../../plugins/apm/server'; +import { APMConfig } from '../../server'; import { LocationProvider } from '../context/LocationContext'; -import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; +import { PromiseReturnType } from '../../typings/common'; import { ESFilter, ESSearchResponse, ESSearchRequest -} from '../../../../../plugins/apm/typings/elasticsearch'; +} from '../../typings/elasticsearch'; import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; export function toJson(wrapper: ReactWrapper) { diff --git a/x-pack/legacy/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md similarity index 92% rename from x-pack/legacy/plugins/apm/readme.md rename to x-pack/plugins/apm/readme.md index e8e2514c83fcb..62465e920d793 100644 --- a/x-pack/legacy/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -32,7 +32,7 @@ _Docker Compose is required_ ### E2E (Cypress) tests ```sh -x-pack/legacy/plugins/apm/e2e/run-e2e.sh +x-pack/plugins/apm/e2e/run-e2e.sh ``` _Starts Kibana (:5701), APM Server (:8201) and Elasticsearch (:9201). Ingests sample data into Elasticsearch via APM Server and runs the Cypress tests_ @@ -94,13 +94,13 @@ _Note: Run the following commands from `kibana/`._ #### Prettier ``` -yarn prettier "./x-pack/legacy/plugins/apm/**/*.{tsx,ts,js}" --write +yarn prettier "./x-pack/plugins/apm/**/*.{tsx,ts,js}" --write ``` #### ESLint ``` -yarn eslint ./x-pack/legacy/plugins/apm --fix +yarn eslint ./x-pack/plugins/apm --fix ``` ### Setup default APM users @@ -117,7 +117,7 @@ For testing purposes APM uses 3 custom users: To create the users with the correct roles run the following script: ```sh -node x-pack/legacy/plugins/apm/scripts/setup-kibana-security.js --role-suffix +node x-pack/plugins/apm/scripts/setup-kibana-security.js --role-suffix ``` The users will be created with the password specified in kibana.dev.yml for `elasticsearch.password` diff --git a/x-pack/legacy/plugins/apm/scripts/.gitignore b/x-pack/plugins/apm/scripts/.gitignore similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/.gitignore rename to x-pack/plugins/apm/scripts/.gitignore diff --git a/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts rename to x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts diff --git a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig.js b/x-pack/plugins/apm/scripts/optimize-tsconfig.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/optimize-tsconfig.js rename to x-pack/plugins/apm/scripts/optimize-tsconfig.js diff --git a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/optimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/optimize.js rename to x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js diff --git a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/paths.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/paths.js rename to x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js diff --git a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/tsconfig.json b/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json similarity index 60% rename from x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/tsconfig.json rename to x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json index 8f6b0f35e4b52..5e05d3962eccb 100644 --- a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/tsconfig.json +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json @@ -1,11 +1,10 @@ { "include": [ "./plugins/apm/**/*", - "./legacy/plugins/apm/**/*", "./typings/**/*" ], "exclude": [ "**/__fixtures__/**/*", - "./legacy/plugins/apm/e2e/cypress/**/*" + "./plugins/apm/e2e/cypress/**/*" ] } diff --git a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/unoptimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/unoptimize.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/unoptimize.js rename to x-pack/plugins/apm/scripts/optimize-tsconfig/unoptimize.js diff --git a/x-pack/legacy/plugins/apm/scripts/package.json b/x-pack/plugins/apm/scripts/package.json similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/package.json rename to x-pack/plugins/apm/scripts/package.json diff --git a/x-pack/legacy/plugins/apm/scripts/setup-kibana-security.js b/x-pack/plugins/apm/scripts/setup-kibana-security.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/setup-kibana-security.js rename to x-pack/plugins/apm/scripts/setup-kibana-security.js diff --git a/x-pack/legacy/plugins/apm/scripts/storybook.js b/x-pack/plugins/apm/scripts/storybook.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/storybook.js rename to x-pack/plugins/apm/scripts/storybook.js diff --git a/x-pack/legacy/plugins/apm/scripts/unoptimize-tsconfig.js b/x-pack/plugins/apm/scripts/unoptimize-tsconfig.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/unoptimize-tsconfig.js rename to x-pack/plugins/apm/scripts/unoptimize-tsconfig.js diff --git a/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data.js b/x-pack/plugins/apm/scripts/upload-telemetry-data.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/upload-telemetry-data.js rename to x-pack/plugins/apm/scripts/upload-telemetry-data.js diff --git a/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/download-telemetry-template.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/download-telemetry-template.ts similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/download-telemetry-template.ts rename to x-pack/plugins/apm/scripts/upload-telemetry-data/download-telemetry-template.ts diff --git a/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts similarity index 97% rename from x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts rename to x-pack/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts index 8d76063a7fdf6..390609996874b 100644 --- a/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts +++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts @@ -18,7 +18,7 @@ import { CollectTelemetryParams, collectDataTelemetry // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/apm/server/lib/apm_telemetry/collect_data_telemetry'; +} from '../../server/lib/apm_telemetry/collect_data_telemetry'; interface GenerateOptions { days: number; diff --git a/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts similarity index 98% rename from x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/index.ts rename to x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts index bdc57eac412fc..4f69a3a3bd213 100644 --- a/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/index.ts +++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts @@ -25,7 +25,7 @@ import { Logger } from 'kibana/server'; // @ts-ignore import consoleStamp from 'console-stamp'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { CollectTelemetryParams } from '../../../../../plugins/apm/server/lib/apm_telemetry/collect_data_telemetry'; +import { CollectTelemetryParams } from '../../server/lib/apm_telemetry/collect_data_telemetry'; import { downloadTelemetryTemplate } from './download-telemetry-template'; import mapping from '../../mappings.json'; import { generateSampleDocuments } from './generate-sample-documents'; diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts new file mode 100644 index 0000000000000..ae0f5510cd80e --- /dev/null +++ b/x-pack/plugins/apm/server/feature.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 { i18n } from '@kbn/i18n'; + +export const APM_FEATURE = { + id: 'apm', + name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { + defaultMessage: 'APM' + }), + order: 900, + icon: 'apmApp', + navLinkId: 'apm', + app: ['apm', 'kibana'], + catalogue: ['apm'], + // see x-pack/plugins/features/common/feature_kibana_privileges.ts + privileges: { + all: { + app: ['apm', 'kibana'], + api: [ + 'apm', + 'apm_write', + 'actions-read', + 'actions-all', + 'alerting-read', + 'alerting-all' + ], + catalogue: ['apm'], + savedObject: { + all: ['alert', 'action', 'action_task_params'], + read: [] + }, + ui: [ + 'show', + 'save', + 'alerting:show', + 'actions:show', + 'alerting:save', + 'actions:save', + 'alerting:delete', + 'actions:delete' + ] + }, + read: { + app: ['apm', 'kibana'], + api: [ + 'apm', + 'actions-read', + 'actions-all', + 'alerting-read', + 'alerting-all' + ], + catalogue: ['apm'], + savedObject: { + all: ['alert', 'action', 'action_task_params'], + read: [] + }, + ui: [ + 'show', + 'alerting:show', + 'actions:show', + 'alerting:save', + 'actions:save', + 'alerting:delete', + 'actions:delete' + ] + } + } +}; diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 77655568a7e9c..9009008790631 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -73,4 +73,4 @@ export type APMConfig = ReturnType; export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); -export { APMPlugin, APMPluginContract } from './plugin'; +export { APMPlugin, APMPluginSetup } from './plugin'; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/queries.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/queries.test.ts index 0d539a09f091b..fcc456c653303 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/queries.test.ts @@ -8,7 +8,7 @@ import { getErrorDistribution } from './get_distribution'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../../public/utils/testHelpers'; describe('error distribution queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/lib/errors/queries.test.ts b/x-pack/plugins/apm/server/lib/errors/queries.test.ts index 5b063c2fb2b61..f1e5d31efd4bd 100644 --- a/x-pack/plugins/apm/server/lib/errors/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/queries.test.ts @@ -9,7 +9,7 @@ import { getErrorGroups } from './get_error_groups'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; describe('error queries', () => { let mock: SearchParamsMock; 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 c22084dbb7168..de9ab87b69fc8 100644 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/es_client.ts @@ -20,7 +20,7 @@ import { ESSearchResponse } from '../../../typings/elasticsearch'; import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; -import { pickKeys } from '../../../../../legacy/plugins/apm/public/utils/pickKeys'; +import { pickKeys } from '../../../common/utils/pick_keys'; import { APMRequestHandlerContext } from '../../routes/typings'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index 8e8cf698a84cf..40a2a0e7216a0 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -39,19 +39,6 @@ function getMockRequest() { _debug: false } }, - __LEGACY: { - server: { - plugins: { - elasticsearch: { - getCluster: jest.fn().mockReturnValue({ callWithInternalUser: {} }) - } - }, - savedObjects: { - SavedObjectsClient: jest.fn(), - getSavedObjectsRepository: jest.fn() - } - } - }, core: { elasticsearch: { dataClient: { diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts new file mode 100644 index 0000000000000..b3b40bac7bd54 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts @@ -0,0 +1,10 @@ +/* + * 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 { APMRequestHandlerContext } from '../../routes/typings'; + +export function getApmIndexPatternTitle(context: APMRequestHandlerContext) { + return context.config['apm_oss.indexPattern']; +} diff --git a/x-pack/plugins/apm/server/lib/metrics/queries.test.ts b/x-pack/plugins/apm/server/lib/metrics/queries.test.ts index ac4e13ae442cf..f276fa69e20e3 100644 --- a/x-pack/plugins/apm/server/lib/metrics/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/metrics/queries.test.ts @@ -12,7 +12,7 @@ import { getThreadCountChart } from './by_agent/java/thread_count'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; describe('metrics queries', () => { diff --git a/x-pack/plugins/apm/server/lib/service_nodes/queries.test.ts b/x-pack/plugins/apm/server/lib/service_nodes/queries.test.ts index 672d568b3a5e3..80cd94b1549d7 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/queries.test.ts @@ -13,7 +13,7 @@ import { getServiceNodes } from './'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; import { getServiceNodeMetadata } from '../services/get_service_node_metadata'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts index 0e52982c6de28..614014ee37afc 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts @@ -7,7 +7,7 @@ import { getServiceAnnotations } from '.'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../../public/utils/testHelpers'; import noVersions from './__fixtures__/no_versions.json'; import oneVersion from './__fixtures__/one_version.json'; import multipleVersions from './__fixtures__/multiple_versions.json'; diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index e75d9ad05648c..16490ace77744 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -12,7 +12,7 @@ import { hasHistoricalAgentData } from './get_services/has_historical_agent_data import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; describe('services queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts index b951b7f350eed..a1a915e6a84a5 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts @@ -12,7 +12,7 @@ import { searchConfigurations } from './search_configurations'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../../public/utils/testHelpers'; import { findExactConfiguration } from './find_exact_configuration'; describe('agent configuration queries', () => { diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts index 9bd6c78797605..5d0bf329368f7 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts @@ -5,7 +5,7 @@ */ import { Setup } from '../../helpers/setup_request'; -import { mockNow } from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +import { mockNow } from '../../../../public/utils/testHelpers'; import { CustomLink } from '../../../../common/custom_link/custom_link_types'; import { createOrUpdateCustomLink } from './create_or_update_custom_link'; diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.test.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.test.ts index 514e473b8e78c..0254660e3523f 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.test.ts @@ -6,7 +6,7 @@ import { inspectSearchParams, SearchParamsMock -} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../../public/utils/testHelpers'; import { getTransaction } from './get_transaction'; import { Setup } from '../../helpers/setup_request'; import { diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.test.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.test.ts index 45610e36786b7..6a67c30bee197 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.test.ts @@ -8,7 +8,7 @@ import { listCustomLinks } from './list_custom_links'; import { inspectSearchParams, SearchParamsMock -} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../../public/utils/testHelpers'; import { Setup } from '../../helpers/setup_request'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/traces/queries.test.ts b/x-pack/plugins/apm/server/lib/traces/queries.test.ts index 0c2ac4d0f9201..871d0fd1c7fb6 100644 --- a/x-pack/plugins/apm/server/lib/traces/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/traces/queries.test.ts @@ -8,7 +8,7 @@ import { getTraceItems } from './get_trace_items'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; describe('trace queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts index 8d6495c2e0b5f..73122d8580134 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts @@ -8,7 +8,7 @@ import { transactionGroupsFetcher } from './fetcher'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; describe('transaction group queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts index 116738da5ef9b..a9e4204fde1ad 100644 --- a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts @@ -11,7 +11,7 @@ import { getTransaction } from './get_transaction'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; describe('transaction queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts index 21cc35da72cb9..b72186f528f28 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts @@ -8,7 +8,7 @@ import { getLocalUIFilters } from './'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../../public/utils/testHelpers'; import { getServicesProjection } from '../../../../common/projections/services'; describe('local ui filter queries', () => { diff --git a/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts index 63c8c3e494bb0..079ab64f32db3 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts @@ -8,7 +8,7 @@ import { getEnvironments } from './get_environments'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; describe('ui filter queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index b434d41982f4c..8cf29de5b8b73 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -10,10 +10,8 @@ import { CoreStart, Logger } from 'src/core/server'; -import { Observable, combineLatest, AsyncSubject } from 'rxjs'; +import { Observable, combineLatest } from 'rxjs'; import { map, take } from 'rxjs/operators'; -import { Server } from 'hapi'; -import { once } from 'lodash'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { TaskManagerSetupContract } from '../../task_manager/server'; import { AlertingPlugin } from '../../alerting/server'; @@ -31,24 +29,20 @@ import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_ import { LicensingPluginSetup } from '../../licensing/public'; import { registerApmAlerts } from './lib/alerts/register_apm_alerts'; import { createApmTelemetry } from './lib/apm_telemetry'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../../plugins/features/server'; +import { APM_FEATURE } from './feature'; +import { apmIndices, apmTelemetry } from './saved_objects'; -export interface LegacySetup { - server: Server; -} - -export interface APMPluginContract { +export interface APMPluginSetup { config$: Observable; - registerLegacyAPI: (__LEGACY: LegacySetup) => void; getApmIndices: () => ReturnType; } -export class APMPlugin implements Plugin { +export class APMPlugin implements Plugin { private currentConfig?: APMConfig; private logger?: Logger; - legacySetup$: AsyncSubject; constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; - this.legacySetup$ = new AsyncSubject(); } public async setup( @@ -62,6 +56,7 @@ export class APMPlugin implements Plugin { taskManager?: TaskManagerSetupContract; alerting?: AlertingPlugin['setup']; actions?: ActionsPlugin['setup']; + features: FeaturesPluginSetup; } ) { this.logger = this.initContext.logger.get(); @@ -70,6 +65,9 @@ export class APMPlugin implements Plugin { map(([apmOssConfig, apmConfig]) => mergeConfigs(apmOssConfig, apmConfig)) ); + core.savedObjects.registerType(apmIndices); + core.savedObjects.registerType(apmTelemetry); + if (plugins.actions && plugins.alerting) { registerApmAlerts({ alerting: plugins.alerting, @@ -78,14 +76,6 @@ export class APMPlugin implements Plugin { }); } - this.legacySetup$.subscribe(__LEGACY => { - createApmApi().init(core, { - config$: mergedConfig$, - logger: this.logger!, - __LEGACY - }); - }); - this.currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); if ( @@ -116,13 +106,15 @@ export class APMPlugin implements Plugin { } }) ); + plugins.features.registerFeature(APM_FEATURE); + + createApmApi().init(core, { + config$: mergedConfig$, + logger: this.logger! + }); return { config$: mergedConfig$, - registerLegacyAPI: once((__LEGACY: LegacySetup) => { - this.legacySetup$.next(__LEGACY); - this.legacySetup$.complete(); - }), getApmIndices: async () => getApmIndices({ savedObjectsClient: await getInternalSavedObjectsClient(core), diff --git a/x-pack/plugins/apm/server/routes/create_api/index.test.ts b/x-pack/plugins/apm/server/routes/create_api/index.test.ts index 312dae1d1f9d2..20c586868a979 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.test.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.test.ts @@ -9,7 +9,6 @@ import { CoreSetup, Logger } from 'src/core/server'; import { Params } from '../typings'; import { BehaviorSubject } from 'rxjs'; import { APMConfig } from '../..'; -import { LegacySetup } from '../../plugin'; const getCoreMock = () => { const get = jest.fn(); @@ -40,8 +39,7 @@ const getCoreMock = () => { config$: new BehaviorSubject({} as APMConfig), logger: ({ error: jest.fn() - } as unknown) as Logger, - __LEGACY: {} as LegacySetup + } as unknown) as Logger } }; }; diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index e216574f8a02e..a97e2f30fc2b6 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -30,7 +30,7 @@ export function createApi() { factoryFns.push(fn); return this as any; }, - init(core, { config$, logger, __LEGACY }) { + init(core, { config$, logger }) { const router = core.http.createRouter(); let config = {} as APMConfig; @@ -136,7 +136,6 @@ export function createApi() { request, context: { ...context, - __LEGACY, // Only return values for parameters that have runtime types, // but always include query as _debug is always set even if // it's not defined in the route. diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 57b3f282852c4..7964d8b0268e8 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -6,7 +6,8 @@ import { staticIndexPatternRoute, - dynamicIndexPatternRoute + dynamicIndexPatternRoute, + apmIndexPatternTitleRoute } from './index_pattern'; import { errorDistributionRoute, @@ -73,6 +74,7 @@ const createApmApi = () => { // index pattern .add(staticIndexPatternRoute) .add(dynamicIndexPatternRoute) + .add(apmIndexPatternTitleRoute) // Errors .add(errorDistributionRoute) diff --git a/x-pack/plugins/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts index b7964dc8e91ed..e296057203ff1 100644 --- a/x-pack/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/plugins/apm/server/routes/index_pattern.ts @@ -8,6 +8,7 @@ import { createStaticIndexPattern } from '../lib/index_pattern/create_static_ind import { createRoute } from './create_route'; import { setupRequest } from '../lib/helpers/setup_request'; import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; +import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title'; export const staticIndexPatternRoute = createRoute(core => ({ method: 'POST', @@ -38,3 +39,10 @@ export const dynamicIndexPatternRoute = createRoute(() => ({ return { dynamicIndexPattern }; } })); + +export const apmIndexPatternTitleRoute = createRoute(() => ({ + path: '/api/apm/index_pattern/title', + handler: async ({ context }) => { + return getApmIndexPatternTitle(context); + } +})); diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 3dc485630c180..6543f2015599b 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -14,7 +14,8 @@ import { import { PickByValue, Optional } from 'utility-types'; import { Observable } from 'rxjs'; import { Server } from 'hapi'; -import { FetchOptions } from '../../../../legacy/plugins/apm/public/services/rest/callApi'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FetchOptions } from '../../public/services/rest/callApi'; import { APMConfig } from '..'; export interface Params { @@ -61,9 +62,6 @@ export type APMRequestHandlerContext< params: { query: { _debug: boolean } } & TDecodedParams; config: APMConfig; logger: Logger; - __LEGACY: { - server: APMLegacyServer; - }; }; export type RouteFactoryFn< @@ -107,7 +105,6 @@ export interface ServerAPI { context: { config$: Observable; logger: Logger; - __LEGACY: { server: Server }; } ) => void; } diff --git a/x-pack/plugins/apm/server/saved_objects/apm_indices.ts b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts new file mode 100644 index 0000000000000..c641f4546aae7 --- /dev/null +++ b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts @@ -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 { SavedObjectsType } from 'src/core/server'; + +export const apmIndices: SavedObjectsType = { + name: 'apm-indices', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + 'apm_oss.sourcemapIndices': { + type: 'keyword' + }, + 'apm_oss.errorIndices': { + type: 'keyword' + }, + 'apm_oss.onboardingIndices': { + type: 'keyword' + }, + 'apm_oss.spanIndices': { + type: 'keyword' + }, + 'apm_oss.transactionIndices': { + type: 'keyword' + }, + 'apm_oss.metricsIndices': { + type: 'keyword' + } + } + } +}; diff --git a/x-pack/plugins/apm/server/saved_objects/apm_telemetry.ts b/x-pack/plugins/apm/server/saved_objects/apm_telemetry.ts new file mode 100644 index 0000000000000..f711e85076e14 --- /dev/null +++ b/x-pack/plugins/apm/server/saved_objects/apm_telemetry.ts @@ -0,0 +1,921 @@ +/* + * 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 { SavedObjectsType } from 'src/core/server'; + +export const apmTelemetry: SavedObjectsType = { + name: 'apm-telemetry', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + agents: { + properties: { + dotnet: { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + go: { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + java: { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + 'js-base': { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + nodejs: { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + python: { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + ruby: { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + 'rum-js': { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + } + } + }, + counts: { + properties: { + agent_configuration: { + properties: { + all: { + type: 'long' + } + } + }, + error: { + properties: { + '1d': { + type: 'long' + }, + all: { + type: 'long' + } + } + }, + max_error_groups_per_service: { + properties: { + '1d': { + type: 'long' + } + } + }, + max_transaction_groups_per_service: { + properties: { + '1d': { + type: 'long' + } + } + }, + metric: { + properties: { + '1d': { + type: 'long' + }, + all: { + type: 'long' + } + } + }, + onboarding: { + properties: { + '1d': { + type: 'long' + }, + all: { + type: 'long' + } + } + }, + services: { + properties: { + '1d': { + type: 'long' + } + } + }, + sourcemap: { + properties: { + '1d': { + type: 'long' + }, + all: { + type: 'long' + } + } + }, + span: { + properties: { + '1d': { + type: 'long' + }, + all: { + type: 'long' + } + } + }, + traces: { + properties: { + '1d': { + type: 'long' + } + } + }, + transaction: { + properties: { + '1d': { + type: 'long' + }, + all: { + type: 'long' + } + } + } + } + }, + cardinality: { + properties: { + user_agent: { + properties: { + original: { + properties: { + all_agents: { + properties: { + '1d': { + type: 'long' + } + } + }, + rum: { + properties: { + '1d': { + type: 'long' + } + } + } + } + } + } + }, + transaction: { + properties: { + name: { + properties: { + all_agents: { + properties: { + '1d': { + type: 'long' + } + } + }, + rum: { + properties: { + '1d': { + type: 'long' + } + } + } + } + } + } + } + } + }, + has_any_services: { + type: 'boolean' + }, + indices: { + properties: { + all: { + properties: { + total: { + properties: { + docs: { + properties: { + count: { + type: 'long' + } + } + }, + store: { + properties: { + size_in_bytes: { + type: 'long' + } + } + } + } + } + } + }, + shards: { + properties: { + total: { + type: 'long' + } + } + } + } + }, + integrations: { + properties: { + ml: { + properties: { + all_jobs_count: { + type: 'long' + } + } + } + } + }, + retainment: { + properties: { + error: { + properties: { + ms: { + type: 'long' + } + } + }, + metric: { + properties: { + ms: { + type: 'long' + } + } + }, + onboarding: { + properties: { + ms: { + type: 'long' + } + } + }, + span: { + properties: { + ms: { + type: 'long' + } + } + }, + transaction: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + services_per_agent: { + properties: { + dotnet: { + type: 'long', + null_value: 0 + }, + go: { + type: 'long', + null_value: 0 + }, + java: { + type: 'long', + null_value: 0 + }, + 'js-base': { + type: 'long', + null_value: 0 + }, + nodejs: { + type: 'long', + null_value: 0 + }, + python: { + type: 'long', + null_value: 0 + }, + ruby: { + type: 'long', + null_value: 0 + }, + 'rum-js': { + type: 'long', + null_value: 0 + } + } + }, + tasks: { + properties: { + agent_configuration: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + agents: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + cardinality: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + groupings: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + indices_stats: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + integrations: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + processor_events: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + services: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + versions: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + } + } + }, + version: { + properties: { + apm_server: { + properties: { + major: { + type: 'long' + }, + minor: { + type: 'long' + }, + patch: { + type: 'long' + } + } + } + } + } + } as SavedObjectsType['mappings']['properties'] + } +}; diff --git a/x-pack/plugins/apm/server/saved_objects/index.ts b/x-pack/plugins/apm/server/saved_objects/index.ts new file mode 100644 index 0000000000000..30c557c526356 --- /dev/null +++ b/x-pack/plugins/apm/server/saved_objects/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 { apmIndices } from './apm_indices'; +export { apmTelemetry } from './apm_telemetry'; diff --git a/x-pack/plugins/apm/typings/common.d.ts b/x-pack/plugins/apm/typings/common.d.ts index bdd2c75f161e9..eeeb85cd1e7c3 100644 --- a/x-pack/plugins/apm/typings/common.d.ts +++ b/x-pack/plugins/apm/typings/common.d.ts @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../legacy/plugins/infra/types/rison_node'; -import '../../../legacy/plugins/infra/types/eui'; +import '../../../typings/rison_node'; +import '../../infra/types/eui'; // EUIBasicTable -import '../../../legacy/plugins/reporting/public/components/report_listing'; -// .svg -import '../../../legacy/plugins/canvas/types/webpack'; +import '../../reporting/public/components/report_listing'; // Allow unknown properties in an object export type AllowUnknownProperties = T extends Array diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx index 206e9821190fb..a8597b7073c95 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx @@ -9,7 +9,7 @@ import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } f import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; import { useVisibilityState } from '../../../utils/use_visibility_state'; -import { getTraceUrl } from '../../../../../../legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks'; +import { getTraceUrl } from '../../../../../apm/public'; import { LogEntriesItem } from '../../../../common/http_api'; import { useLinkProps, LinkDescriptor } from '../../../hooks/use_link_props'; import { decodeOrThrow } from '../../../../common/runtime_types'; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 038fd457fb6c7..4bbbf8dcdee03 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -11,7 +11,7 @@ import { RouteMethod, RouteConfig } from '../../../../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../plugins/features/server'; import { SpacesPluginSetup } from '../../../../../../plugins/spaces/server'; import { VisTypeTimeseriesSetup } from '../../../../../../../src/plugins/vis_type_timeseries/server'; -import { APMPluginContract } from '../../../../../../plugins/apm/server'; +import { APMPluginSetup } from '../../../../../../plugins/apm/server'; import { HomeServerPluginSetup } from '../../../../../../../src/plugins/home/server'; import { PluginSetupContract as AlertingPluginContract } from '../../../../../../plugins/alerting/server'; @@ -22,7 +22,7 @@ export interface InfraServerPluginDeps { usageCollection: UsageCollectionSetup; visTypeTimeseries: VisTypeTimeseriesSetup; features: FeaturesPluginSetup; - apm: APMPluginContract; + apm: APMPluginSetup; alerting: AlertingPluginContract; } diff --git a/x-pack/plugins/siem/scripts/optimize_tsconfig/README.md b/x-pack/plugins/siem/scripts/optimize_tsconfig/README.md index 2b402367c1db3..fbcd3329312aa 100644 --- a/x-pack/plugins/siem/scripts/optimize_tsconfig/README.md +++ b/x-pack/plugins/siem/scripts/optimize_tsconfig/README.md @@ -1,5 +1,5 @@ Hard forked from here: -x-pack/legacy/plugins/apm/scripts/optimize-tsconfig.js +x-pack/plugins/apm/scripts/optimize-tsconfig.js #### Optimizing TypeScript diff --git a/x-pack/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts b/x-pack/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts index 5b667f461fc60..78aadf75e54c3 100644 --- a/x-pack/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts +++ b/x-pack/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts @@ -5,7 +5,7 @@ */ /* ** Applying the same logic as: - ** x-pack/legacy/plugins/apm/server/lib/helpers/get_bucket_size/calculate_auto.js + ** x-pack/plugins/apm/server/lib/helpers/get_bucket_size/calculate_auto.js */ import moment from 'moment'; import { get } from 'lodash/fp'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4cd152f2d297d..28e290c90687b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4184,7 +4184,6 @@ "xpack.apm.alertTypes.errorRate": "エラー率", "xpack.apm.alertTypes.transactionDuration": "トランザクション期間", "xpack.apm.apmDescription": "アプリケーション内から自動的に詳細なパフォーマンスメトリックやエラーを集めます。", - "xpack.apm.apmForESDescription": "Elastic Stack 用の APM", "xpack.apm.applyFilter": "{title} フィルターを適用", "xpack.apm.applyOptions": "オプションを適用", "xpack.apm.breadcrumb.errorsTitle": "エラー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 619ea0646eb44..321fd533aab6d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4185,7 +4185,6 @@ "xpack.apm.alertTypes.errorRate": "错误率", "xpack.apm.alertTypes.transactionDuration": "事务持续时间", "xpack.apm.apmDescription": "自动从您的应用程序内收集深入全面的性能指标和错误。", - "xpack.apm.apmForESDescription": "Elastic Stack 的 APM", "xpack.apm.applyFilter": "应用 {title} 筛选", "xpack.apm.applyOptions": "应用选项", "xpack.apm.breadcrumb.errorsTitle": "错误", diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js index 936eae04ffa64..282cf1311a7a9 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js @@ -14,7 +14,8 @@ import { units, fontSizes, unit, -} from '../../../../../../../legacy/plugins/apm/public/style/variables'; + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../apm/public/style/variables'; import { tint } from 'polished'; import theme from '@elastic/eui/dist/eui_theme_light.json'; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js index ac6832050b9d3..0fdf8c3400562 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js @@ -9,7 +9,8 @@ import PropTypes from 'prop-types'; import styled from 'styled-components'; import { isEmpty } from 'lodash'; import Suggestion from './suggestion'; -import { units, px, unit } from '../../../../../../../legacy/plugins/apm/public/style/variables'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { units, px, unit } from '../../../../../../apm/public/style/variables'; import { tint } from 'polished'; import theme from '@elastic/eui/dist/eui_theme_light.json'; diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index a540c7e3c9786..81b29732377da 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -12,7 +12,7 @@ "exclude": [ "test/**/*", "plugins/siem/cypress/**/*", - "legacy/plugins/apm/e2e/cypress/**/*", + "plugins/apm/e2e/cypress/**/*", "**/typespec_tests.ts" ], "compilerOptions": { From 47b8ba5d5b80e51cfe7263ef60390463a25a1673 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 1 May 2020 00:20:00 +0300 Subject: [PATCH 053/122] [SIEM][CASE] Refactor Connectors - Jira Connector (#63450) --- x-pack/plugins/actions/README.md | 60 +- .../server/builtin_action_types/case/api.ts | 93 ++ .../{servicenow => case}/constants.ts | 1 - .../builtin_action_types/case/schema.ts | 98 ++ .../case/transformers.test.ts | 131 +++ .../builtin_action_types/case/transformers.ts | 29 + .../builtin_action_types/case/translations.ts | 55 ++ .../server/builtin_action_types/case/types.ts | 161 ++++ .../helpers.test.ts => case/utils.test.ts} | 319 +++++-- .../server/builtin_action_types/case/utils.ts | 249 +++++ .../builtin_action_types/case/validators.ts | 35 + .../server/builtin_action_types/index.ts | 6 +- .../builtin_action_types/jira/api.test.ts | 517 +++++++++++ .../server/builtin_action_types/jira/api.ts | 7 + .../builtin_action_types/jira/config.ts | 13 + .../server/builtin_action_types/jira/index.ts | 24 + .../server/builtin_action_types/jira/mocks.ts | 124 +++ .../builtin_action_types/jira/schema.ts | 22 + .../builtin_action_types/jira/service.test.ts | 297 ++++++ .../builtin_action_types/jira/service.ts | 156 ++++ .../builtin_action_types/jira/translations.ts | 11 + .../server/builtin_action_types/jira/types.ts | 32 + .../builtin_action_types/jira/validators.ts | 13 + .../servicenow/action_handlers.test.ts | 850 ------------------ .../servicenow/action_handlers.ts | 129 --- .../servicenow/api.test.ts | 523 +++++++++++ .../builtin_action_types/servicenow/api.ts | 7 + .../builtin_action_types/servicenow/config.ts | 13 + .../servicenow/helpers.ts | 125 --- .../servicenow/index.test.ts | 268 ------ .../builtin_action_types/servicenow/index.ts | 124 +-- .../servicenow/lib/constants.ts | 13 - .../servicenow/lib/index.test.ts | 334 ------- .../servicenow/lib/index.ts | 186 ---- .../servicenow/lib/types.ts | 32 - .../builtin_action_types/servicenow/mock.ts | 115 --- .../builtin_action_types/servicenow/mocks.ts | 117 +++ .../builtin_action_types/servicenow/schema.ts | 70 -- .../servicenow/service.test.ts | 255 ++++++ .../servicenow/service.ts | 138 +++ .../servicenow/transformers.ts | 43 - .../servicenow/translations.ts | 73 +- .../builtin_action_types/servicenow/types.ts | 102 +-- .../servicenow/validators.ts | 13 + x-pack/plugins/case/common/api/cases/case.ts | 41 +- x-pack/plugins/case/common/constants.ts | 2 + .../api/cases/configure/get_connectors.ts | 11 +- .../siem/public/containers/case/api.test.tsx | 4 +- .../siem/public/containers/case/api.ts | 4 +- .../siem/public/containers/case/mock.ts | 7 +- .../case/use_post_push_to_service.test.tsx | 4 +- .../case/use_post_push_to_service.tsx | 6 +- .../components/connector_flyout/index.tsx | 148 +++ .../siem/public/lib/connectors/config.ts | 16 +- .../siem/public/lib/connectors/index.ts | 1 + .../siem/public/lib/connectors/jira/config.ts | 20 + .../public/lib/connectors/jira/flyout.tsx | 110 +++ .../siem/public/lib/connectors/jira/index.tsx | 54 ++ .../{logos/servicenow.svg => jira/logo.svg} | 0 .../lib/connectors/jira/translations.ts | 28 + .../siem/public/lib/connectors/jira/types.ts | 18 + .../siem/public/lib/connectors/servicenow.tsx | 246 ----- .../lib/connectors/servicenow/config.ts | 20 + .../lib/connectors/servicenow/flyout.tsx | 83 ++ .../lib/connectors/servicenow/index.tsx | 48 + .../public/lib/connectors/servicenow/logo.svg | 5 + .../lib/connectors/servicenow/translations.ts | 23 + .../public/lib/connectors/servicenow/types.ts | 18 + .../public/lib/connectors/translations.ts | 65 +- .../siem/public/lib/connectors/types.ts | 44 +- .../siem/public/lib/connectors/utils.ts | 71 ++ .../configure_cases/connectors_dropdown.tsx | 4 +- .../components/configure_cases/index.test.tsx | 10 + .../case/components/configure_cases/index.tsx | 13 +- x-pack/plugins/siem/public/plugin.tsx | 3 +- .../translations/translations/ja-JP.json | 18 - .../translations/translations/zh-CN.json | 18 - .../alerting_api_integration/common/config.ts | 1 + .../plugins/actions_simulators/index.ts | 9 +- .../actions_simulators/jira_simulation.ts | 101 +++ .../servicenow_simulation.ts | 8 +- .../actions/builtin_action_types/jira.ts | 549 +++++++++++ .../builtin_action_types/servicenow.ts | 750 +++++++++------- .../tests/actions/index.ts | 1 + 84 files changed, 5330 insertions(+), 3235 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/api.ts rename x-pack/plugins/actions/server/builtin_action_types/{servicenow => case}/constants.ts (87%) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/schema.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/translations.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/types.ts rename x-pack/plugins/actions/server/builtin_action_types/{servicenow/helpers.test.ts => case/utils.test.ts} (56%) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/utils.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/case/validators.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/api.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/config.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/index.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/service.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/types.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts create mode 100644 x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx create mode 100644 x-pack/plugins/siem/public/lib/connectors/jira/config.ts create mode 100644 x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx create mode 100644 x-pack/plugins/siem/public/lib/connectors/jira/index.tsx rename x-pack/plugins/siem/public/lib/connectors/{logos/servicenow.svg => jira/logo.svg} (100%) mode change 100755 => 100644 create mode 100644 x-pack/plugins/siem/public/lib/connectors/jira/translations.ts create mode 100644 x-pack/plugins/siem/public/lib/connectors/jira/types.ts delete mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow.tsx create mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts create mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx create mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx create mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg create mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts create mode 100644 x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts create mode 100644 x-pack/plugins/siem/public/lib/connectors/utils.ts create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/jira_simulation.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index decd170ca5dd6..4c8cc3aa503e6 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -28,7 +28,7 @@ Table of Contents - [RESTful API](#restful-api) - [`POST /api/action`: Create action](#post-apiaction-create-action) - [`DELETE /api/action/{id}`: Delete action](#delete-apiactionid-delete-action) - - [`GET /api/action/_getAll`: Get all actions](#get-apiaction-get-all-actions) + - [`GET /api/action/_getAll`: Get all actions](#get-apiactiongetall-get-all-actions) - [`GET /api/action/{id}`: Get action](#get-apiactionid-get-action) - [`GET /api/action/types`: List action types](#get-apiactiontypes-list-action-types) - [`PUT /api/action/{id}`: Update action](#put-apiactionid-update-action) @@ -64,6 +64,12 @@ Table of Contents - [`config`](#config-6) - [`secrets`](#secrets-6) - [`params`](#params-6) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice) + - [Jira](#jira) + - [`config`](#config-7) + - [`secrets`](#secrets-7) + - [`params`](#params-7) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1) - [Command Line Utility](#command-line-utility) ## Terminology @@ -143,8 +149,8 @@ This is the primary function for an action type. Whenever the action needs to ex | actionId | The action saved object id that the action type is executing for. | | config | The decrypted configuration given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. | | params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. | -| services.callCluster(path, opts) | Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana but runs in the context of the user who is calling the action when security is enabled.| -| services.getScopedCallCluster | This function scopes an instance of CallCluster by returning a `callCluster(path, opts)` function that runs in the context of the user who is calling the action when security is enabled. This must only be called with instances of CallCluster provided by core.| +| services.callCluster(path, opts) | Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana but runs in the context of the user who is calling the action when security is enabled. | +| services.getScopedCallCluster | This function scopes an instance of CallCluster by returning a `callCluster(path, opts)` function that runs in the context of the user who is calling the action when security is enabled. This must only be called with instances of CallCluster provided by core. | | services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | | services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) | @@ -483,13 +489,59 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a ### `params` +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------------------ | ------ | +| subAction | The sub action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | +| subActionParams | The parameters of the sub action | object | + +#### `subActionParams (pushToService)` + | Property | Description | Type | | ----------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- | | caseId | The case id | string | | title | The title of the case | string _(optional)_ | | description | The description of the case | string _(optional)_ | | comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | -| incidentID | The id of the incident in ServiceNow . If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| externalId | The id of the incident in ServiceNow . If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | + +--- + +## Jira + +ID: `.jira` + +The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/platform/rest/v2/) to create and update Jira incidents. + +### `config` + +| Property | Description | Type | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| apiUrl | ServiceNow instance URL. | string | +| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object | + +### `secrets` + +| Property | Description | Type | +| -------- | --------------------------------------- | ------ | +| email | email for HTTP Basic authentication | string | +| apiToken | API token for HTTP Basic authentication | string | + +### `params` + +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------------------ | ------ | +| subAction | The sub action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | +| subActionParams | The parameters of the sub action | object | + +#### `subActionParams (pushToService)` + +| Property | Description | Type | +| ----------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | +| caseId | The case id | string | +| title | The title of the case | string _(optional)_ | +| description | The description of the case | string _(optional)_ | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | +| externalId | The id of the incident in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | # Command Line Utility diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/api.ts b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts new file mode 100644 index 0000000000000..6dc8a9cc9af6a --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts @@ -0,0 +1,93 @@ +/* + * 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 { + ExternalServiceApi, + ExternalServiceParams, + PushToServiceResponse, + GetIncidentApiHandlerArgs, + HandshakeApiHandlerArgs, + PushToServiceApiHandlerArgs, +} from './types'; +import { prepareFieldsForTransformation, transformFields, transformComments } from './utils'; + +const handshakeHandler = async ({ + externalService, + mapping, + params, +}: HandshakeApiHandlerArgs) => {}; +const getIncidentHandler = async ({ + externalService, + mapping, + params, +}: GetIncidentApiHandlerArgs) => {}; + +const pushToServiceHandler = async ({ + externalService, + mapping, + params, +}: PushToServiceApiHandlerArgs): Promise => { + const { externalId, comments } = params; + const updateIncident = externalId ? true : false; + const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; + let currentIncident: ExternalServiceParams | undefined; + let res: PushToServiceResponse; + + if (externalId) { + currentIncident = await externalService.getIncident(externalId); + } + + const fields = prepareFieldsForTransformation({ + params, + mapping, + defaultPipes, + }); + + const incident = transformFields({ + params, + fields, + currentIncident, + }); + + if (updateIncident) { + res = await externalService.updateIncident({ incidentId: externalId, incident }); + } else { + res = await externalService.createIncident({ incident }); + } + + if ( + comments && + Array.isArray(comments) && + comments.length > 0 && + mapping.get('comments')?.actionType !== 'nothing' + ) { + const commentsTransformed = transformComments(comments, ['informationAdded']); + + res.comments = []; + for (const currentComment of commentsTransformed) { + const comment = await externalService.createComment({ + incidentId: res.id, + comment: currentComment, + field: mapping.get('comments')?.target ?? 'comments', + }); + res.comments = [ + ...(res.comments ?? []), + { + commentId: comment.commentId, + pushedDate: comment.pushedDate, + }, + ]; + } + } + + return res; +}; + +export const api: ExternalServiceApi = { + handshake: handshakeHandler, + pushToService: pushToServiceHandler, + getIncident: getIncidentHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/case/constants.ts similarity index 87% rename from x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/constants.ts index a0ffd859e14ca..1f2bc7f5e8e53 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/constants.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ACTION_TYPE_ID = '.servicenow'; export const SUPPORTED_SOURCE_FIELDS = ['title', 'comments', 'description']; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts new file mode 100644 index 0000000000000..33b2ad6d18684 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts @@ -0,0 +1,98 @@ +/* + * 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'; + +export const MappingActionType = schema.oneOf([ + schema.literal('nothing'), + schema.literal('overwrite'), + schema.literal('append'), +]); + +export const MapRecordSchema = schema.object({ + source: schema.string(), + target: schema.string(), + actionType: MappingActionType, +}); + +export const CaseConfigurationSchema = schema.object({ + mapping: schema.arrayOf(MapRecordSchema), +}); + +export const ExternalIncidentServiceConfiguration = { + apiUrl: schema.string(), + casesConfiguration: CaseConfigurationSchema, +}; + +export const ExternalIncidentServiceConfigurationSchema = schema.object( + ExternalIncidentServiceConfiguration +); + +export const ExternalIncidentServiceSecretConfiguration = { + password: schema.string(), + username: schema.string(), +}; + +export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( + ExternalIncidentServiceSecretConfiguration +); + +export const UserSchema = schema.object({ + fullName: schema.nullable(schema.string()), + username: schema.nullable(schema.string()), +}); + +const EntityInformation = { + createdAt: schema.string(), + createdBy: UserSchema, + updatedAt: schema.nullable(schema.string()), + updatedBy: schema.nullable(UserSchema), +}; + +export const EntityInformationSchema = schema.object(EntityInformation); + +export const CommentSchema = schema.object({ + commentId: schema.string(), + comment: schema.string(), + ...EntityInformation, +}); + +export const ExecutorSubActionSchema = schema.oneOf([ + schema.literal('getIncident'), + schema.literal('pushToService'), + schema.literal('handshake'), +]); + +export const ExecutorSubActionPushParamsSchema = schema.object({ + caseId: schema.string(), + title: schema.string(), + description: schema.nullable(schema.string()), + comments: schema.nullable(schema.arrayOf(CommentSchema)), + externalId: schema.nullable(schema.string()), + ...EntityInformation, +}); + +export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ + externalId: schema.string(), +}); + +// Reserved for future implementation +export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); + +export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('getIncident'), + subActionParams: ExecutorSubActionGetIncidentParamsSchema, + }), + schema.object({ + subAction: schema.literal('handshake'), + subActionParams: ExecutorSubActionHandshakeParamsSchema, + }), + schema.object({ + subAction: schema.literal('pushToService'), + subActionParams: ExecutorSubActionPushParamsSchema, + }), +]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts new file mode 100644 index 0000000000000..75dcab65ee9f2 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { transformers } from './transformers'; + +const { informationCreated, informationUpdated, informationAdded, append } = transformers; + +describe('informationCreated', () => { + test('transforms correctly', () => { + const res = informationCreated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + }); + expect(res).toEqual({ value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)' }); + }); + + test('transforms correctly without optional fields', () => { + const res = informationCreated({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value (created at by )' }); + }); + + test('returns correctly rest fields', () => { + const res = informationCreated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)', + previousValue: 'previous value', + }); + }); +}); + +describe('informationUpdated', () => { + test('transforms correctly', () => { + const res = informationUpdated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + }); + expect(res).toEqual({ value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)' }); + }); + + test('transforms correctly without optional fields', () => { + const res = informationUpdated({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value (updated at by )' }); + }); + + test('returns correctly rest fields', () => { + const res = informationUpdated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)', + previousValue: 'previous value', + }); + }); +}); + +describe('informationAdded', () => { + test('transforms correctly', () => { + const res = informationAdded({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + }); + expect(res).toEqual({ value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)' }); + }); + + test('transforms correctly without optional fields', () => { + const res = informationAdded({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value (added at by )' }); + }); + + test('returns correctly rest fields', () => { + const res = informationAdded({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)', + previousValue: 'previous value', + }); + }); +}); + +describe('append', () => { + test('transforms correctly', () => { + const res = append({ + value: 'a value', + previousValue: 'previous value', + }); + expect(res).toEqual({ value: 'previous value \r\na value' }); + }); + + test('transforms correctly without optional fields', () => { + const res = append({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value' }); + }); + + test('returns correctly rest fields', () => { + const res = append({ + value: 'a value', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'previous value \r\na value', + user: 'elastic', + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts new file mode 100644 index 0000000000000..3dca1fd703430 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TransformerArgs } from './types'; +import * as i18n from './translations'; + +export type Transformer = (args: TransformerArgs) => TransformerArgs; + +export const transformers: Record = { + informationCreated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`, + ...rest, + }), + informationUpdated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`, + ...rest, + }), + informationAdded: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`, + ...rest, + }), + append: ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ + value: previousValue ? `${previousValue} \r\n${value}` : `${value}`, + ...rest, + }), +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts new file mode 100644 index 0000000000000..4842728b0e4e7 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts @@ -0,0 +1,55 @@ +/* + * 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 API_URL_REQUIRED = i18n.translate('xpack.actions.builtin.case.connectorApiNullError', { + defaultMessage: 'connector [apiUrl] is required', +}); + +export const FIELD_INFORMATION = ( + mode: string, + date: string | undefined, + user: string | undefined +) => { + switch (mode) { + case 'create': + return i18n.translate('xpack.actions.builtin.case.common.externalIncidentCreated', { + values: { date, user }, + defaultMessage: '(created at {date} by {user})', + }); + case 'update': + return i18n.translate('xpack.actions.builtin.case.common.externalIncidentUpdated', { + values: { date, user }, + defaultMessage: '(updated at {date} by {user})', + }); + case 'add': + return i18n.translate('xpack.actions.builtin.case.common.externalIncidentAdded', { + values: { date, user }, + defaultMessage: '(added at {date} by {user})', + }); + default: + return i18n.translate('xpack.actions.builtin.case.common.externalIncidentDefault', { + values: { date, user }, + defaultMessage: '(created at {date} by {user})', + }); + } +}; + +export const MAPPING_EMPTY = i18n.translate( + 'xpack.actions.builtin.case.configuration.emptyMapping', + { + defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', + } +); + +export const WHITE_LISTED_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.case.configuration.apiWhitelistError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts new file mode 100644 index 0000000000000..459e9d2b03f92 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// This will have to remain `any` until we can extend connectors with generics +// and circular dependencies eliminated. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { TypeOf } from '@kbn/config-schema'; + +import { + ExternalIncidentServiceConfigurationSchema, + ExternalIncidentServiceSecretConfigurationSchema, + ExecutorParamsSchema, + CaseConfigurationSchema, + MapRecordSchema, + CommentSchema, + ExecutorSubActionPushParamsSchema, + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, +} from './schema'; + +export interface AnyParams { + [index: string]: string | number | object | undefined | null; +} + +export type ExternalIncidentServiceConfiguration = TypeOf< + typeof ExternalIncidentServiceConfigurationSchema +>; +export type ExternalIncidentServiceSecretConfiguration = TypeOf< + typeof ExternalIncidentServiceSecretConfigurationSchema +>; + +export type ExecutorParams = TypeOf; +export type ExecutorSubActionPushParams = TypeOf; + +export type ExecutorSubActionGetIncidentParams = TypeOf< + typeof ExecutorSubActionGetIncidentParamsSchema +>; + +export type ExecutorSubActionHandshakeParams = TypeOf< + typeof ExecutorSubActionHandshakeParamsSchema +>; + +export type CaseConfiguration = TypeOf; +export type MapRecord = TypeOf; +export type Comment = TypeOf; + +export interface ExternalServiceConfiguration { + id: string; + name: string; +} + +export interface ExternalServiceCredentials { + config: Record; + secrets: Record; +} + +export interface ExternalServiceValidation { + config: (configurationUtilities: any, configObject: any) => void; + secrets: (configurationUtilities: any, secrets: any) => void; +} + +export interface ExternalServiceIncidentResponse { + id: string; + title: string; + url: string; + pushedDate: string; +} + +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} + +export interface ExternalServiceParams { + [index: string]: any; +} + +export interface ExternalService { + getIncident: (id: string) => Promise; + createIncident: (params: ExternalServiceParams) => Promise; + updateIncident: (params: ExternalServiceParams) => Promise; + createComment: (params: ExternalServiceParams) => Promise; +} + +export interface PushToServiceApiParams extends ExecutorSubActionPushParams { + externalCase: Record; +} + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; + mapping: Map; +} + +export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: PushToServiceApiParams; +} + +export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionGetIncidentParams; +} + +export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionHandshakeParams; +} + +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface ExternalServiceApi { + handshake: (args: HandshakeApiHandlerArgs) => Promise; + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; + getIncident: (args: GetIncidentApiHandlerArgs) => Promise; +} + +export interface CreateExternalServiceBasicArgs { + api: ExternalServiceApi; + createExternalService: (credentials: ExternalServiceCredentials) => ExternalService; +} + +export interface CreateExternalServiceArgs extends CreateExternalServiceBasicArgs { + config: ExternalServiceConfiguration; + validate: ExternalServiceValidation; + validationSchema: { config: any; secrets: any }; +} + +export interface CreateActionTypeArgs { + configurationUtilities: any; + executor?: any; +} + +export interface PipedField { + key: string; + value: string; + actionType: string; + pipes: string[]; +} + +export interface PrepareFieldsForTransformArgs { + params: PushToServiceApiParams; + mapping: Map; + defaultPipes?: string[]; +} + +export interface TransformFieldsArgs { + params: PushToServiceApiParams; + fields: PipedField[]; + currentIncident?: ExternalServiceParams; +} + +export interface TransformerArgs { + value: string; + date?: string; + user?: string; + previousValue?: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts similarity index 56% rename from x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts rename to x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts index cbcefe6364e8f..1e8cc3eda20e5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts @@ -4,28 +4,65 @@ * you may not use this file except in compliance with the Elastic License. */ +import axios from 'axios'; + import { normalizeMapping, buildMap, mapParams, - appendField, - appendInformationToField, prepareFieldsForTransformation, transformFields, transformComments, -} from './helpers'; -import { mapping, finalMapping } from './mock'; + addTimeZoneToDate, + throwIfNotAlive, + request, + patch, + getErrorMessage, +} from './utils'; + import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { MapEntry, Params, Comment } from './types'; +import { Comment, MapRecord, PushToServiceApiParams } from './types'; + +jest.mock('axios'); +const axiosMock = (axios as unknown) as jest.Mock; + +const mapping: MapRecord[] = [ + { source: 'title', target: 'short_description', actionType: 'overwrite' }, + { source: 'description', target: 'description', actionType: 'append' }, + { source: 'comments', target: 'comments', actionType: 'append' }, +]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const finalMapping: Map = new Map(); + +finalMapping.set('title', { + target: 'short_description', + actionType: 'overwrite', +}); -const maliciousMapping: MapEntry[] = [ +finalMapping.set('description', { + target: 'description', + actionType: 'append', +}); + +finalMapping.set('comments', { + target: 'comments', + actionType: 'append', +}); + +finalMapping.set('short_description', { + target: 'title', + actionType: 'overwrite', +}); + +const maliciousMapping: MapRecord[] = [ { source: '__proto__', target: 'short_description', actionType: 'nothing' }, { source: 'description', target: '__proto__', actionType: 'nothing' }, { source: 'comments', target: 'comments', actionType: 'nothing' }, { source: 'unsupportedSource', target: 'comments', actionType: 'nothing' }, ]; -const fullParams: Params = { +const fullParams: PushToServiceApiParams = { caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', title: 'a title', description: 'a description', @@ -33,15 +70,14 @@ const fullParams: Params = { createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, - incidentId: null, - incident: { + externalId: null, + externalCase: { short_description: 'a title', description: 'a description', }, comments: [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -50,7 +86,6 @@ const fullParams: Params = { }, { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'second comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -60,7 +95,7 @@ const fullParams: Params = { ], }; -describe('sanitizeMapping', () => { +describe('normalizeMapping', () => { test('remove malicious fields', () => { const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); expect(sanitizedMapping.every(m => m.source !== '__proto__' && m.target !== '__proto__')).toBe( @@ -194,7 +229,10 @@ describe('transformFields', () => { params: { ...fullParams, updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: 'Another User' }, + updatedBy: { + username: 'anotherUser', + fullName: 'Another User', + }, }, mapping: finalMapping, defaultPipes: ['informationUpdated'], @@ -204,7 +242,10 @@ describe('transformFields', () => { params: { ...fullParams, updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: 'Another User' }, + updatedBy: { + username: 'anotherUser', + fullName: 'Another User', + }, }, fields, currentIncident: { @@ -244,7 +285,10 @@ describe('transformFields', () => { }); const res = transformFields({ - params: { ...fullParams, createdBy: { fullName: null, username: 'elastic' } }, + params: { + ...fullParams, + createdBy: { fullName: '', username: 'elastic' }, + }, fields, }); @@ -259,7 +303,10 @@ describe('transformFields', () => { params: { ...fullParams, updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: 'Another User' }, + updatedBy: { + username: 'anotherUser', + fullName: 'Another User', + }, }, mapping: finalMapping, defaultPipes: ['informationUpdated'], @@ -269,7 +316,7 @@ describe('transformFields', () => { params: { ...fullParams, updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: null }, + updatedBy: { username: 'anotherUser', fullName: '' }, }, fields, }); @@ -281,60 +328,11 @@ describe('transformFields', () => { }); }); -describe('appendField', () => { - test('prefix correctly', () => { - expect('my_prefixmy_value ').toEqual(appendField({ value: 'my_value', prefix: 'my_prefix' })); - }); - - test('suffix correctly', () => { - expect('my_value my_suffix').toEqual(appendField({ value: 'my_value', suffix: 'my_suffix' })); - }); - - test('prefix and suffix correctly', () => { - expect('my_prefixmy_value my_suffix').toEqual( - appendField({ value: 'my_value', prefix: 'my_prefix', suffix: 'my_suffix' }) - ); - }); -}); - -describe('appendInformationToField', () => { - test('creation mode', () => { - const res = appendInformationToField({ - value: 'my value', - user: 'Elastic Test User', - date: '2020-03-13T08:34:53.450Z', - mode: 'create', - }); - expect(res).toEqual('my value (created at 2020-03-13T08:34:53.450Z by Elastic Test User)'); - }); - - test('update mode', () => { - const res = appendInformationToField({ - value: 'my value', - user: 'Elastic Test User', - date: '2020-03-13T08:34:53.450Z', - mode: 'update', - }); - expect(res).toEqual('my value (updated at 2020-03-13T08:34:53.450Z by Elastic Test User)'); - }); - - test('add mode', () => { - const res = appendInformationToField({ - value: 'my value', - user: 'Elastic Test User', - date: '2020-03-13T08:34:53.450Z', - mode: 'add', - }); - expect(res).toEqual('my value (added at 2020-03-13T08:34:53.450Z by Elastic Test User)'); - }); -}); - describe('transformComments', () => { test('transform creation comments', () => { const comments: Comment[] = [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -342,11 +340,10 @@ describe('transformComments', () => { updatedBy: null, }, ]; - const res = transformComments(comments, fullParams, ['informationCreated']); + const res = transformComments(comments, ['informationCreated']); expect(res).toEqual([ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -360,32 +357,36 @@ describe('transformComments', () => { const comments: Comment[] = [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, + updatedBy: { + fullName: 'Another User', + username: 'anotherUser', + }, }, ]; - const res = transformComments(comments, fullParams, ['informationUpdated']); + const res = transformComments(comments, ['informationUpdated']); expect(res).toEqual([ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment (updated at 2020-03-15T08:34:53.450Z by Another User)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, + updatedBy: { + fullName: 'Another User', + username: 'anotherUser', + }, }, ]); }); + test('transform added comments', () => { const comments: Comment[] = [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -393,11 +394,10 @@ describe('transformComments', () => { updatedBy: null, }, ]; - const res = transformComments(comments, fullParams, ['informationAdded']); + const res = transformComments(comments, ['informationAdded']); expect(res).toEqual([ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -406,4 +406,171 @@ describe('transformComments', () => { }, ]); }); + + test('transform comments without fullname', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: '', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by elastic)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: '', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]); + }); + + test('adds update user correctly', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic', username: 'elastic' }, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic2', username: 'elastic' }, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment (added at 2020-04-13T08:34:53.450Z by Elastic2)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic', username: 'elastic' }, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic2', username: 'elastic' }, + }, + ]); + }); + + test('adds update user with empty fullname correctly', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic', username: 'elastic' }, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: '', username: 'elastic2' }, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment (added at 2020-04-13T08:34:53.450Z by elastic2)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic', username: 'elastic' }, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: '', username: 'elastic2' }, + }, + ]); + }); +}); + +describe('addTimeZoneToDate', () => { + test('adds timezone with default', () => { + const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z'); + expect(date).toBe('2020-04-14T15:01:55.456Z GMT'); + }); + + test('adds timezone correctly', () => { + const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z', 'PST'); + expect(date).toBe('2020-04-14T15:01:55.456Z PST'); + }); +}); + +describe('throwIfNotAlive ', () => { + test('throws correctly when status is invalid', async () => { + expect(() => { + throwIfNotAlive(404, 'application/json'); + }).toThrow('Instance is not alive.'); + }); + + test('throws correctly when content is invalid', () => { + expect(() => { + throwIfNotAlive(200, 'application/html'); + }).toThrow('Instance is not alive.'); + }); + + test('do NOT throws with custom validStatusCodes', async () => { + expect(() => { + throwIfNotAlive(404, 'application/json', [404]); + }).not.toThrow('Instance is not alive.'); + }); +}); + +describe('request', () => { + beforeEach(() => { + axiosMock.mockImplementation(() => ({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + })); + }); + + test('it fetch correctly with defaults', async () => { + const res = await request({ axios, url: '/test' }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {} }); + expect(res).toEqual({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + }); + }); + + test('it fetch correctly', async () => { + const res = await request({ axios, url: '/test', method: 'post', data: { id: '123' } }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' } }); + expect(res).toEqual({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + }); + }); + + test('it throws correctly', async () => { + axiosMock.mockImplementation(() => ({ + status: 404, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + })); + + await expect(request({ axios, url: '/test' })).rejects.toThrow(); + }); +}); + +describe('patch', () => { + beforeEach(() => { + axiosMock.mockImplementation(() => ({ + status: 200, + headers: { 'content-type': 'application/json' }, + })); + }); + + test('it fetch correctly', async () => { + await patch({ axios, url: '/test', data: { id: '123' } }); + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } }); + }); +}); + +describe('getErrorMessage', () => { + test('it returns the correct error message', () => { + const msg = getErrorMessage('My connector name', 'An error has occurred'); + expect(msg).toBe('[Action][My connector name]: An error has occurred'); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts new file mode 100644 index 0000000000000..7d69b2791f624 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -0,0 +1,249 @@ +/* + * 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 { curry, flow, get } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { AxiosInstance, Method, AxiosResponse } from 'axios'; + +import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types'; + +import { ExecutorParamsSchema } from './schema'; + +import { + CreateExternalServiceArgs, + CreateActionTypeArgs, + ExecutorParams, + MapRecord, + AnyParams, + CreateExternalServiceBasicArgs, + PrepareFieldsForTransformArgs, + PipedField, + TransformFieldsArgs, + Comment, + ExecutorSubActionPushParams, +} from './types'; + +import { transformers, Transformer } from './transformers'; + +import { SUPPORTED_SOURCE_FIELDS } from './constants'; + +export const normalizeMapping = (supportedFields: string[], mapping: MapRecord[]): MapRecord[] => { + // Prevent prototype pollution and remove unsupported fields + return mapping.filter( + m => m.source !== '__proto__' && m.target !== '__proto__' && supportedFields.includes(m.source) + ); +}; + +export const buildMap = (mapping: MapRecord[]): Map => { + return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { + const { source, target, actionType } = field; + fieldsMap.set(source, { target, actionType }); + fieldsMap.set(target, { target: source, actionType }); + return fieldsMap; + }, new Map()); +}; + +export const mapParams = ( + params: Partial, + mapping: Map +): AnyParams => { + return Object.keys(params).reduce((prev: AnyParams, curr: string): AnyParams => { + const field = mapping.get(curr); + if (field) { + prev[field.target] = get(params, curr); + } + return prev; + }, {}); +}; + +export const createConnectorExecutor = ({ + api, + createExternalService, +}: CreateExternalServiceBasicArgs) => async ( + execOptions: ActionTypeExecutorOptions +): Promise => { + const { actionId, config, params, secrets } = execOptions; + const { subAction, subActionParams } = params as ExecutorParams; + let data = {}; + + const res: Pick & + Pick = { + status: 'ok', + actionId, + }; + + const externalService = createExternalService({ + config, + secrets, + }); + + if (!api[subAction]) { + throw new Error('[Action][ExternalService] Unsupported subAction type.'); + } + + if (subAction !== 'pushToService') { + throw new Error('[Action][ExternalService] subAction not implemented.'); + } + + if (subAction === 'pushToService') { + const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; + const { comments, externalId, ...restParams } = pushToServiceParams; + + const mapping = buildMap(config.casesConfiguration.mapping); + const externalCase = mapParams(restParams, mapping); + + data = await api.pushToService({ + externalService, + mapping, + params: { ...pushToServiceParams, externalCase }, + }); + } + + return { + ...res, + data, + }; +}; + +export const createConnector = ({ + api, + config, + validate, + createExternalService, + validationSchema, +}: CreateExternalServiceArgs) => { + return ({ + configurationUtilities, + executor = createConnectorExecutor({ api, createExternalService }), + }: CreateActionTypeArgs): ActionType => ({ + id: config.id, + name: config.name, + minimumLicenseRequired: 'platinum', + validate: { + config: schema.object(validationSchema.config, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(validationSchema.secrets, { + validate: curry(validate.secrets)(configurationUtilities), + }), + params: ExecutorParamsSchema, + }, + executor, + }); +}; + +export const throwIfNotAlive = ( + status: number, + contentType: string, + validStatusCodes: number[] = [200, 201, 204] +) => { + if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { + throw new Error('Instance is not alive.'); + } +}; + +export const request = async ({ + axios, + url, + method = 'get', + data, +}: { + axios: AxiosInstance; + url: string; + method?: Method; + data?: T; +}): Promise => { + const res = await axios(url, { method, data: data ?? {} }); + throwIfNotAlive(res.status, res.headers['content-type']); + return res; +}; + +export const patch = async ({ + axios, + url, + data, +}: { + axios: AxiosInstance; + url: string; + data: T; +}): Promise => { + return request({ + axios, + url, + method: 'patch', + data, + }); +}; + +export const addTimeZoneToDate = (date: string, timezone = 'GMT'): string => { + return `${date} ${timezone}`; +}; + +export const prepareFieldsForTransformation = ({ + params, + mapping, + defaultPipes = ['informationCreated'], +}: PrepareFieldsForTransformArgs): PipedField[] => { + return Object.keys(params.externalCase) + .filter(p => mapping.get(p)?.actionType != null && mapping.get(p)?.actionType !== 'nothing') + .map(p => { + const actionType = mapping.get(p)?.actionType ?? 'nothing'; + return { + key: p, + value: params.externalCase[p], + actionType, + pipes: actionType === 'append' ? [...defaultPipes, 'append'] : defaultPipes, + }; + }); +}; + +export const transformFields = ({ + params, + fields, + currentIncident, +}: TransformFieldsArgs): Record => { + return fields.reduce((prev, cur) => { + const transform = flow(...cur.pipes.map(p => transformers[p])); + return { + ...prev, + [cur.key]: transform({ + value: cur.value, + date: params.updatedAt ?? params.createdAt, + user: + (params.updatedBy != null + ? params.updatedBy.fullName + ? params.updatedBy.fullName + : params.updatedBy.username + : params.createdBy.fullName + ? params.createdBy.fullName + : params.createdBy.username) ?? '', + previousValue: currentIncident ? currentIncident[cur.key] : '', + }).value, + }; + }, {}); +}; + +export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { + return comments.map(c => ({ + ...c, + comment: flow(...pipes.map(p => transformers[p]))({ + value: c.comment, + date: c.updatedAt ?? c.createdAt, + user: + (c.updatedBy != null + ? c.updatedBy.fullName + ? c.updatedBy.fullName + : c.updatedBy.username + : c.createdBy.fullName + ? c.createdBy.fullName + : c.createdBy.username) ?? '', + }).value, + })); +}; + +export const getErrorMessage = (connector: string, msg: string) => { + return `[Action][${connector}]: ${msg}`; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts new file mode 100644 index 0000000000000..80e301e5be082 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash'; + +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, +} from './types'; + +import * as i18n from './translations'; + +export const validateCommonConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: ExternalIncidentServiceConfiguration +) => { + try { + if (isEmpty(configObject.casesConfiguration.mapping)) { + return i18n.MAPPING_EMPTY; + } + + configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); + } catch (whitelistError) { + return i18n.WHITE_LISTED_ERROR(whitelistError.message); + } +}; + +export const validateCommonSecrets = ( + configurationUtilities: ActionsConfigurationUtilities, + secrets: ExternalIncidentServiceSecretConfiguration +) => {}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index a92a279d08439..6ba4d7cfc7de0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -12,9 +12,10 @@ import { getActionType as getEmailActionType } from './email'; import { getActionType as getIndexActionType } from './es_index'; import { getActionType as getPagerDutyActionType } from './pagerduty'; import { getActionType as getServerLogActionType } from './server_log'; -import { getActionType as getServiceNowActionType } from './servicenow'; import { getActionType as getSlackActionType } from './slack'; import { getActionType as getWebhookActionType } from './webhook'; +import { getActionType as getServiceNowActionType } from './servicenow'; +import { getActionType as getJiraActionType } from './jira'; export function registerBuiltInActionTypes({ actionsConfigUtils: configurationUtilities, @@ -29,7 +30,8 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getIndexActionType({ logger })); actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServerLogActionType({ logger })); - actionTypeRegistry.register(getServiceNowActionType({ configurationUtilities })); actionTypeRegistry.register(getSlackActionType({ configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); + actionTypeRegistry.register(getServiceNowActionType({ configurationUtilities })); + actionTypeRegistry.register(getJiraActionType({ configurationUtilities })); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts new file mode 100644 index 0000000000000..bcfb82077d286 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts @@ -0,0 +1,517 @@ +/* + * 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 { api } from '../case/api'; +import { externalServiceMock, mapping, apiParams } from './mocks'; +import { ExternalService } from '../case/types'; + +describe('api', () => { + let externalService: jest.Mocked; + + beforeEach(() => { + externalService = externalServiceMock.create(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('pushToService', () => { + describe('create incident', () => { + test('it creates an incident', async () => { + const params = { ...apiParams, externalId: null }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-04-27T10:59:46.202Z', + }, + ], + }); + }); + + test('it creates an incident without comments', async () => { + const params = { ...apiParams, externalId: null, comments: [] }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + }); + + test('it calls createIncident correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + description: + 'Incident description (created at 2020-04-27T10:59:46.202Z by Elastic User)', + summary: 'Incident title (created at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + }); + }); + + describe('update incident', () => { + test('it updates an incident', async () => { + const res = await api.pushToService({ externalService, mapping, params: apiParams }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-04-27T10:59:46.202Z', + }, + ], + }); + }); + + test('it updates an incident without comments', async () => { + const params = { ...apiParams, comments: [] }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + }); + + test('it calls updateIncident correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + expect(externalService.createIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + }); + }); + + describe('mapping variations', () => { + test('overwrite & append', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('nothing & append', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('append & append', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: + 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('nothing & nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: {}, + }); + }); + + test('overwrite & nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('overwrite & overwrite', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('nothing & overwrite', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('append & overwrite', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: + 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('append & nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: + 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('comment nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'nothing', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.createComment).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts new file mode 100644 index 0000000000000..3db66e5884af4 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { api } from '../case/api'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts new file mode 100644 index 0000000000000..7e415109f1bd9 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/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 { ExternalServiceConfiguration } from '../case/types'; +import * as i18n from './translations'; + +export const config: ExternalServiceConfiguration = { + id: '.jira', + name: i18n.NAME, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts new file mode 100644 index 0000000000000..a2d7bb5930a75 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -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. + */ + +import { createConnector } from '../case/utils'; + +import { api } from './api'; +import { config } from './config'; +import { validate } from './validators'; +import { createExternalService } from './service'; +import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema'; + +export const getActionType = createConnector({ + api, + config, + validate, + createExternalService, + validationSchema: { + config: JiraPublicConfiguration, + secrets: JiraSecretConfiguration, + }, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts new file mode 100644 index 0000000000000..3ae0e9db36de0 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts @@ -0,0 +1,124 @@ +/* + * 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 { + ExternalService, + PushToServiceApiParams, + ExecutorSubActionPushParams, + MapRecord, +} from '../case/types'; + +const createMock = (): jest.Mocked => { + const service = { + getIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + key: 'CK-1', + summary: 'title from jira', + description: 'description from jira', + created: '2020-04-27T10:59:46.202Z', + updated: '2020-04-27T10:59:46.202Z', + }) + ), + createIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }) + ), + updateIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }) + ), + createComment: jest.fn(), + }; + + service.createComment.mockImplementationOnce(() => + Promise.resolve({ + commentId: 'case-comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + externalCommentId: '1', + }) + ); + + service.createComment.mockImplementationOnce(() => + Promise.resolve({ + commentId: 'case-comment-2', + pushedDate: '2020-04-27T10:59:46.202Z', + externalCommentId: '2', + }) + ); + + return service; +}; + +const externalServiceMock = { + create: createMock, +}; + +const mapping: Map> = new Map(); + +mapping.set('title', { + target: 'summary', + actionType: 'overwrite', +}); + +mapping.set('description', { + target: 'description', + actionType: 'overwrite', +}); + +mapping.set('comments', { + target: 'comments', + actionType: 'append', +}); + +mapping.set('summary', { + target: 'title', + actionType: 'overwrite', +}); + +const executorParams: ExecutorSubActionPushParams = { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + externalId: 'incident-3', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + title: 'Incident title', + description: 'Incident description', + comments: [ + { + commentId: 'case-comment-1', + comment: 'A comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + }, + { + commentId: 'case-comment-2', + comment: 'Another comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + }, + ], +}; + +const apiParams: PushToServiceApiParams = { + ...executorParams, + externalCase: { summary: 'Incident title', description: 'Incident description' }, +}; + +export { externalServiceMock, mapping, executorParams, apiParams }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts new file mode 100644 index 0000000000000..9c831e75d91c1 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { ExternalIncidentServiceConfiguration } from '../case/schema'; + +export const JiraPublicConfiguration = { + projectKey: schema.string(), + ...ExternalIncidentServiceConfiguration, +}; + +export const JiraPublicConfigurationSchema = schema.object(JiraPublicConfiguration); + +export const JiraSecretConfiguration = { + email: schema.string(), + apiToken: schema.string(), +}; + +export const JiraSecretConfigurationSchema = schema.object(JiraSecretConfiguration); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts new file mode 100644 index 0000000000000..b9225b043d526 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -0,0 +1,297 @@ +/* + * 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 axios from 'axios'; + +import { createExternalService } from './service'; +import * as utils from '../case/utils'; +import { ExternalService } from '../case/types'; + +jest.mock('axios'); +jest.mock('../case/utils', () => { + const originalUtils = jest.requireActual('../case/utils'); + return { + ...originalUtils, + request: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; + +describe('Jira service', () => { + let service: ExternalService; + + beforeAll(() => { + service = createExternalService({ + config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK' }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createExternalService', () => { + test('throws without url', () => { + expect(() => + createExternalService({ + config: { apiUrl: null, projectKey: 'CK' }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }) + ).toThrow(); + }); + + test('throws without projectKey', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com', projectKey: null }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }) + ).toThrow(); + }); + + test('throws without username', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com' }, + secrets: { apiToken: '', email: 'elastic@elastic.com' }, + }) + ).toThrow(); + }); + + test('throws without password', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com' }, + secrets: { apiToken: '', email: undefined }, + }) + ).toThrow(); + }); + }); + + describe('getIncident', () => { + test('it returns the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + })); + const res = await service.getIncident('1'); + expect(res).toEqual({ id: '1', key: 'CK-1', summary: 'title', description: 'description' }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { id: '1', key: 'CK-1' }, + })); + + await service.getIncident('1'); + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + expect(service.getIncident('1')).rejects.toThrow( + 'Unable to get incident with id 1. Error: An error has occurred' + ); + }); + }); + + describe('createIncident', () => { + test('it creates the incident correctly', async () => { + // The response from Jira when creating an issue contains only the key and the id. + // The service makes two calls when creating an issue. One to create and one to get + // the created incident with all the necessary fields. + requestMock.mockImplementationOnce(() => ({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, + })); + + const res = await service.createIncident({ + incident: { summary: 'title', description: 'desc' }, + }); + + expect(res).toEqual({ + title: 'CK-1', + id: '1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + fields: { created: '2020-04-27T10:59:46.202Z' }, + }, + })); + + await service.createIncident({ + incident: { summary: 'title', description: 'desc' }, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + method: 'post', + data: { + fields: { + summary: 'title', + description: 'desc', + project: { key: 'CK' }, + issuetype: { name: 'Task' }, + }, + }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.createIncident({ + incident: { summary: 'title', description: 'desc' }, + }) + ).rejects.toThrow('[Action][Jira]: Unable to create incident. Error: An error has occurred'); + }); + }); + + describe('updateIncident', () => { + test('it updates the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + fields: { updated: '2020-04-27T10:59:46.202Z' }, + }, + })); + + const res = await service.updateIncident({ + incidentId: '1', + incident: { summary: 'title', description: 'desc' }, + }); + + expect(res).toEqual({ + title: 'CK-1', + id: '1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + fields: { updated: '2020-04-27T10:59:46.202Z' }, + }, + })); + + await service.updateIncident({ + incidentId: '1', + incident: { summary: 'title', description: 'desc' }, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + method: 'put', + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', + data: { fields: { summary: 'title', description: 'desc' } }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.updateIncident({ + incidentId: '1', + incident: { summary: 'title', description: 'desc' }, + }) + ).rejects.toThrow( + '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred' + ); + }); + }); + + describe('createComment', () => { + test('it creates the comment correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }, + })); + + const res = await service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'comments', + }); + + expect(res).toEqual({ + commentId: 'comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + externalCommentId: '1', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }, + })); + + await service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'my_field', + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + method: 'post', + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment', + data: { body: 'comment' }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'comments', + }) + ).rejects.toThrow( + '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred' + ); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts new file mode 100644 index 0000000000000..ff22b8368e7dd --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -0,0 +1,156 @@ +/* + * 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 axios from 'axios'; + +import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; +import { + JiraPublicConfigurationType, + JiraSecretConfigurationType, + CreateIncidentRequest, + UpdateIncidentRequest, + CreateCommentRequest, +} from './types'; + +import * as i18n from './translations'; +import { getErrorMessage, request } from '../case/utils'; + +const VERSION = '2'; +const BASE_URL = `rest/api/${VERSION}`; +const INCIDENT_URL = `issue`; +const COMMENT_URL = `comment`; + +const VIEW_INCIDENT_URL = `browse`; + +export const createExternalService = ({ + config, + secrets, +}: ExternalServiceCredentials): ExternalService => { + const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType; + const { apiToken, email } = secrets as JiraSecretConfigurationType; + + if (!url || !projectKey || !apiToken || !email) { + throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); + } + + const incidentUrl = `${url}/${BASE_URL}/${INCIDENT_URL}`; + const commentUrl = `${incidentUrl}/{issueId}/${COMMENT_URL}`; + const axiosInstance = axios.create({ + auth: { username: email, password: apiToken }, + }); + + const getIncidentViewURL = (key: string) => { + return `${url}/${VIEW_INCIDENT_URL}/${key}`; + }; + + const getCommentsURL = (issueId: string) => { + return commentUrl.replace('{issueId}', issueId); + }; + + const getIncident = async (id: string) => { + try { + const res = await request({ + axios: axiosInstance, + url: `${incidentUrl}/${id}`, + }); + + const { fields, ...rest } = res.data; + + return { ...rest, ...fields }; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`) + ); + } + }; + + const createIncident = async ({ incident }: ExternalServiceParams) => { + // The response from Jira when creating an issue contains only the key and the id. + // The function makes two calls when creating an issue. One to create the issue and one to get + // the created issue with all the necessary fields. + try { + const res = await request({ + axios: axiosInstance, + url: `${incidentUrl}`, + method: 'post', + data: { + fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } }, + }, + }); + + const updatedIncident = await getIncident(res.data.id); + + return { + title: updatedIncident.key, + id: updatedIncident.id, + pushedDate: new Date(updatedIncident.created).toISOString(), + url: getIncidentViewURL(updatedIncident.key), + }; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`) + ); + } + }; + + const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { + try { + await request({ + axios: axiosInstance, + method: 'put', + url: `${incidentUrl}/${incidentId}`, + data: { fields: { ...incident } }, + }); + + const updatedIncident = await getIncident(incidentId); + + return { + title: updatedIncident.key, + id: updatedIncident.id, + pushedDate: new Date(updatedIncident.updated).toISOString(), + url: getIncidentViewURL(updatedIncident.key), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to update incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } + }; + + const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { + try { + const res = await request({ + axios: axiosInstance, + method: 'post', + url: getCommentsURL(incidentId), + data: { body: comment.comment }, + }); + + return { + commentId: comment.commentId, + externalCommentId: res.data.id, + pushedDate: new Date(res.data.created).toISOString(), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } + }; + + return { + getIncident, + createIncident, + updateIncident, + createComment, + }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts new file mode 100644 index 0000000000000..dae0d75952e11 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const NAME = i18n.translate('xpack.actions.builtin.case.jiraTitle', { + defaultMessage: 'Jira', +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts new file mode 100644 index 0000000000000..8d9c6b92abb3b --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -0,0 +1,32 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { JiraPublicConfigurationSchema, JiraSecretConfigurationSchema } from './schema'; + +export type JiraPublicConfigurationType = TypeOf; +export type JiraSecretConfigurationType = TypeOf; + +interface CreateIncidentBasicRequestArgs { + summary: string; + description: string; +} +interface CreateIncidentRequestArgs extends CreateIncidentBasicRequestArgs { + project: { key: string }; + issuetype: { name: string }; +} + +export interface CreateIncidentRequest { + fields: CreateIncidentRequestArgs; +} + +export interface UpdateIncidentRequest { + fields: Partial; +} + +export interface CreateCommentRequest { + body: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts new file mode 100644 index 0000000000000..7226071392bc6 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.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 { validateCommonConfig, validateCommonSecrets } from '../case/validators'; +import { ExternalServiceValidation } from '../case/types'; + +export const validate: ExternalServiceValidation = { + config: validateCommonConfig, + secrets: validateCommonSecrets, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts deleted file mode 100644 index aa9b1dcfcf239..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts +++ /dev/null @@ -1,850 +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 { - handleCreateIncident, - handleUpdateIncident, - handleIncident, - createComments, -} from './action_handlers'; -import { ServiceNow } from './lib'; -import { Mapping } from './types'; - -jest.mock('./lib'); - -const ServiceNowMock = ServiceNow as jest.Mock; - -const finalMapping: Mapping = new Map(); - -finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', -}); - -finalMapping.set('description', { - target: 'description', - actionType: 'overwrite', -}); - -finalMapping.set('comments', { - target: 'comments', - actionType: 'append', -}); - -finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', -}); - -const params = { - caseId: '123', - title: 'a title', - description: 'a description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - incidentId: null, - incident: { - short_description: 'a title', - description: 'a description', - }, - comments: [ - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ], -}; - -beforeAll(() => { - ServiceNowMock.mockImplementation(() => { - return { - serviceNow: { - getUserID: jest.fn().mockResolvedValue('1234'), - getIncident: jest.fn().mockResolvedValue({ - short_description: 'servicenow title', - description: 'servicenow desc', - }), - createIncident: jest.fn().mockResolvedValue({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }), - updateIncident: jest.fn().mockResolvedValue({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }), - batchCreateComments: jest - .fn() - .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), - }, - }; - }); -}); - -describe('handleIncident', () => { - test('create an incident', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleIncident({ - incidentId: null, - serviceNow, - params, - comments: params.comments, - mapping: finalMapping, - }); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); - test('update an incident', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleIncident({ - incidentId: '123', - serviceNow, - params, - comments: params.comments, - mapping: finalMapping, - }); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); -}); - -describe('handleCreateIncident', () => { - test('create an incident without comments', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleCreateIncident({ - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.createIncident).toHaveBeenCalled(); - expect(serviceNow.createIncident).toHaveBeenCalledWith({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.createIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('create an incident with comments', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleCreateIncident({ - serviceNow, - params, - comments: params.comments, - mapping: finalMapping, - }); - - expect(serviceNow.createIncident).toHaveBeenCalled(); - expect(serviceNow.createIncident).toHaveBeenCalledWith({ - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.createIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( - '123', - [ - { - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - ], - 'comments' - ); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); -}); - -describe('handleUpdateIncident', () => { - test('update an incident without comments', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params: { - ...params, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', - description: 'a description (updated at 2020-03-15T08:34:53.450Z by Another User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('update an incident with comments', async () => { - const { serviceNow } = new ServiceNowMock(); - serviceNow.batchCreateComments.mockResolvedValue([ - { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }, - { commentId: '789', pushedDate: '2020-03-10T12:24:20.000Z' }, - ]); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params: { - ...params, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - comments: [ - { - comment: 'first comment', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - { - comment: 'second comment', - commentId: '789', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-16T08:34:53.450Z', - updatedBy: { - fullName: 'Another User', - username: 'anotherUser', - }, - version: 'WzU3LDFd', - }, - ], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - description: 'a description (updated at 2020-03-15T08:34:53.450Z by Another User)', - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( - '123', - [ - { - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - { - comment: 'second comment (added at 2020-03-16T08:34:53.450Z by Another User)', - commentId: '789', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-16T08:34:53.450Z', - updatedBy: { - fullName: 'Another User', - username: 'anotherUser', - }, - version: 'WzU3LDFd', - }, - ], - 'comments' - ); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - { - commentId: '789', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); -}); - -describe('handleUpdateIncident: different action types', () => { - test('overwrite & append', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'append', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('nothing & append', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'append', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - description: - 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('append & append', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'append', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: - 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('nothing & nothing', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {}); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('overwrite & nothing', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('overwrite & overwrite', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('nothing & overwrite', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('append & overwrite', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: - 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('append & nothing', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: - 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); -}); - -describe('createComments', () => { - test('create comments correctly', async () => { - const { serviceNow } = new ServiceNowMock(); - serviceNow.batchCreateComments.mockResolvedValue([ - { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }, - { commentId: '789', pushedDate: '2020-03-10T12:24:20.000Z' }, - ]); - - const comments = [ - { - comment: 'first comment', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - { - comment: 'second comment', - commentId: '789', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - version: 'WzU3LDFd', - }, - ]; - - const res = await createComments(serviceNow, '123', 'comments', comments); - - expect(serviceNow.batchCreateComments).toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( - '123', - [ - { - comment: 'first comment', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - { - comment: 'second comment', - commentId: '789', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - version: 'WzU3LDFd', - }, - ], - 'comments' - ); - expect(res).toEqual([ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - { - commentId: '789', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ]); - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts deleted file mode 100644 index 9166f53cf757e..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ /dev/null @@ -1,129 +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 { zipWith } from 'lodash'; -import { CommentResponse } from './lib/types'; -import { - HandlerResponse, - Comment, - SimpleComment, - CreateHandlerArguments, - UpdateHandlerArguments, - IncidentHandlerArguments, -} from './types'; -import { ServiceNow } from './lib'; -import { transformFields, prepareFieldsForTransformation, transformComments } from './helpers'; - -export const createComments = async ( - serviceNow: ServiceNow, - incidentId: string, - key: string, - comments: Comment[] -): Promise => { - const createdComments = await serviceNow.batchCreateComments(incidentId, comments, key); - - return zipWith(comments, createdComments, (a: Comment, b: CommentResponse) => ({ - commentId: a.commentId, - pushedDate: b.pushedDate, - })); -}; - -export const handleCreateIncident = async ({ - serviceNow, - params, - comments, - mapping, -}: CreateHandlerArguments): Promise => { - const fields = prepareFieldsForTransformation({ - params, - mapping, - }); - - const incident = transformFields({ - params, - fields, - }); - - const createdIncident = await serviceNow.createIncident({ - ...incident, - }); - - const res: HandlerResponse = { ...createdIncident }; - - if ( - comments && - Array.isArray(comments) && - comments.length > 0 && - mapping.get('comments')?.actionType !== 'nothing' - ) { - comments = transformComments(comments, params, ['informationAdded']); - res.comments = [ - ...(await createComments( - serviceNow, - res.incidentId, - mapping.get('comments')!.target, - comments - )), - ]; - } - - return { ...res }; -}; - -export const handleUpdateIncident = async ({ - incidentId, - serviceNow, - params, - comments, - mapping, -}: UpdateHandlerArguments): Promise => { - const currentIncident = await serviceNow.getIncident(incidentId); - const fields = prepareFieldsForTransformation({ - params, - mapping, - defaultPipes: ['informationUpdated'], - }); - - const incident = transformFields({ - params, - fields, - currentIncident, - }); - - const updatedIncident = await serviceNow.updateIncident(incidentId, { - ...incident, - }); - - const res: HandlerResponse = { ...updatedIncident }; - - if ( - comments && - Array.isArray(comments) && - comments.length > 0 && - mapping.get('comments')?.actionType !== 'nothing' - ) { - comments = transformComments(comments, params, ['informationAdded']); - res.comments = [ - ...(await createComments(serviceNow, incidentId, mapping.get('comments')!.target, comments)), - ]; - } - - return { ...res }; -}; - -export const handleIncident = async ({ - incidentId, - serviceNow, - params, - comments, - mapping, -}: IncidentHandlerArguments): Promise => { - if (!incidentId) { - return await handleCreateIncident({ serviceNow, params, comments, mapping }); - } else { - return await handleUpdateIncident({ incidentId, serviceNow, params, comments, mapping }); - } -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts new file mode 100644 index 0000000000000..86a8318841271 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -0,0 +1,523 @@ +/* + * 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 { api } from '../case/api'; +import { externalServiceMock, mapping, apiParams } from './mocks'; +import { ExternalService } from '../case/types'; + +describe('api', () => { + let externalService: jest.Mocked; + + beforeEach(() => { + externalService = externalServiceMock.create(); + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('pushToService', () => { + describe('create incident', () => { + test('it creates an incident', async () => { + const params = { ...apiParams, externalId: null }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); + + test('it creates an incident without comments', async () => { + const params = { ...apiParams, externalId: null, comments: [] }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }); + }); + + test('it calls createIncident correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + description: + 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + }); + }); + + describe('update incident', () => { + test('it updates an incident', async () => { + const res = await api.pushToService({ externalService, mapping, params: apiParams }); + + expect(res).toEqual({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); + + test('it updates an incident without comments', async () => { + const params = { ...apiParams, comments: [] }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }); + }); + + test('it calls updateIncident correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + expect(externalService.createIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-2', + comment: { + commentId: 'case-comment-1', + comment: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-2', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + }); + }); + + describe('mapping variations', () => { + test('overwrite & append', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('nothing & append', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('append & append', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('nothing & nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: {}, + }); + }); + + test('overwrite & nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('overwrite & overwrite', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('nothing & overwrite', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('append & overwrite', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('append & nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('comment nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'nothing', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.createComment).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts new file mode 100644 index 0000000000000..3db66e5884af4 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { api } from '../case/api'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts new file mode 100644 index 0000000000000..4ad8108c3b137 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/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 { ExternalServiceConfiguration } from '../case/types'; +import * as i18n from './translations'; + +export const config: ExternalServiceConfiguration = { + id: '.servicenow', + name: i18n.NAME, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts deleted file mode 100644 index 0a26996ea8d69..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ /dev/null @@ -1,125 +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 { flow } from 'lodash'; - -import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { - MapEntry, - Mapping, - AppendFieldArgs, - AppendInformationFieldArgs, - Params, - Comment, - TransformFieldsArgs, - PipedField, - PrepareFieldsForTransformArgs, - KeyAny, -} from './types'; -import { Incident } from './lib/types'; - -import * as transformers from './transformers'; -import * as i18n from './translations'; - -export const normalizeMapping = (supportedFields: string[], mapping: MapEntry[]): MapEntry[] => { - // Prevent prototype pollution and remove unsupported fields - return mapping.filter( - m => m.source !== '__proto__' && m.target !== '__proto__' && supportedFields.includes(m.source) - ); -}; - -export const buildMap = (mapping: MapEntry[]): Mapping => { - return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { - const { source, target, actionType } = field; - fieldsMap.set(source, { target, actionType }); - fieldsMap.set(target, { target: source, actionType }); - return fieldsMap; - }, new Map()); -}; - -export const mapParams = (params: Record, mapping: Mapping) => { - return Object.keys(params).reduce((prev: KeyAny, curr: string): KeyAny => { - const field = mapping.get(curr); - if (field) { - prev[field.target] = params[curr]; - } - return prev; - }, {}); -}; - -export const appendField = ({ value, prefix = '', suffix = '' }: AppendFieldArgs): string => { - return `${prefix}${value} ${suffix}`; -}; - -const t = { ...transformers } as { [index: string]: Function }; // TODO: Find a better solution exists. - -export const prepareFieldsForTransformation = ({ - params, - mapping, - defaultPipes = ['informationCreated'], -}: PrepareFieldsForTransformArgs): PipedField[] => { - return Object.keys(params.incident) - .filter(p => mapping.get(p)!.actionType !== 'nothing') - .map(p => ({ - key: p, - value: params.incident[p] as string, - actionType: mapping.get(p)!.actionType, - pipes: [...defaultPipes], - })) - .map(p => ({ - ...p, - pipes: p.actionType === 'append' ? [...p.pipes, 'append'] : p.pipes, - })); -}; - -export const transformFields = ({ - params, - fields, - currentIncident, -}: TransformFieldsArgs): Incident => { - return fields.reduce((prev: Incident, cur) => { - const transform = flow(...cur.pipes.map(p => t[p])); - prev[cur.key] = transform({ - value: cur.value, - date: params.updatedAt ?? params.createdAt, - user: - params.updatedBy != null - ? params.updatedBy.fullName ?? params.updatedBy.username - : params.createdBy.fullName ?? params.createdBy.username, - previousValue: currentIncident ? currentIncident[cur.key] : '', - }).value; - return prev; - }, {} as Incident); -}; - -export const appendInformationToField = ({ - value, - user, - date, - mode = 'create', -}: AppendInformationFieldArgs): string => { - return appendField({ - value, - suffix: i18n.FIELD_INFORMATION(mode, date, user), - }); -}; - -export const transformComments = ( - comments: Comment[], - params: Params, - pipes: string[] -): Comment[] => { - return comments.map(c => ({ - ...c, - comment: flow(...pipes.map(p => t[p]))({ - value: c.comment, - date: c.updatedAt ?? c.createdAt, - user: - c.updatedBy != null - ? c.updatedBy.fullName ?? c.updatedBy.username - : c.createdBy.fullName ?? c.createdBy.username, - }).value, - })); -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts deleted file mode 100644 index a6c3ae88765ac..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ /dev/null @@ -1,268 +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 { getActionType } from '.'; -import { ActionType, Services, ActionTypeExecutorOptions } from '../../types'; -import { validateConfig, validateSecrets, validateParams } from '../../lib'; -import { createActionTypeRegistry } from '../index.test'; -import { actionsConfigMock } from '../../actions_config.mock'; -import { actionsMock } from '../../mocks'; - -import { ACTION_TYPE_ID } from './constants'; -import * as i18n from './translations'; - -import { handleIncident } from './action_handlers'; -import { incidentResponse } from './mock'; - -jest.mock('./action_handlers'); - -const handleIncidentMock = handleIncident as jest.Mock; - -const services: Services = actionsMock.createServices(); - -let actionType: ActionType; - -const mockOptions = { - name: 'servicenow-connector', - actionTypeId: '.servicenow', - secrets: { - username: 'secret-username', - password: 'secret-password', - }, - config: { - apiUrl: 'https://service-now.com', - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'work_notes', - actionType: 'append', - }, - ], - }, - }, - params: { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', - title: 'Incident title', - description: 'Incident description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - comments: [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ], - }, -}; - -beforeAll(() => { - const { actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); -}); - -describe('get()', () => { - test('should return correct action type', () => { - expect(actionType.id).toEqual(ACTION_TYPE_ID); - expect(actionType.name).toEqual(i18n.NAME); - }); -}); - -describe('validateConfig()', () => { - test('should validate and pass when config is valid', () => { - const { config } = mockOptions; - expect(validateConfig(actionType, config)).toEqual(config); - }); - - test('should validate and throw error when config is invalid', () => { - expect(() => { - validateConfig(actionType, { shouldNotBeHere: true }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]"` - ); - }); - - test('should validate and pass when the servicenow url is whitelisted', () => { - actionType = getActionType({ - configurationUtilities: { - ...actionsConfigMock.create(), - ensureWhitelistedUri: url => { - expect(url).toEqual(mockOptions.config.apiUrl); - }, - }, - }); - - expect(validateConfig(actionType, mockOptions.config)).toEqual(mockOptions.config); - }); - - test('config validation returns an error if the specified URL isnt whitelisted', () => { - actionType = getActionType({ - configurationUtilities: { - ...actionsConfigMock.create(), - ensureWhitelistedUri: _ => { - throw new Error(`target url is not whitelisted`); - }, - }, - }); - - expect(() => { - validateConfig(actionType, mockOptions.config); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: error configuring servicenow action: target url is not whitelisted"` - ); - }); -}); - -describe('validateSecrets()', () => { - test('should validate and pass when secrets is valid', () => { - const { secrets } = mockOptions; - expect(validateSecrets(actionType, secrets)).toEqual(secrets); - }); - - test('should validate and throw error when secrets is invalid', () => { - expect(() => { - validateSecrets(actionType, { username: false }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: [password]: expected value of type [string] but got [undefined]"` - ); - - expect(() => { - validateSecrets(actionType, { username: false, password: 'hello' }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: [username]: expected value of type [string] but got [boolean]"` - ); - }); -}); - -describe('validateParams()', () => { - test('should validate and pass when params is valid', () => { - const { params } = mockOptions; - expect(validateParams(actionType, params)).toEqual(params); - }); - - test('should validate and throw error when params is invalid', () => { - expect(() => { - validateParams(actionType, {}); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [caseId]: expected value of type [string] but got [undefined]"` - ); - }); -}); - -describe('execute()', () => { - beforeEach(() => { - handleIncidentMock.mockReset(); - }); - - test('should create an incident', async () => { - const actionId = 'some-id'; - const { incidentId, ...rest } = mockOptions.params; - - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { ...rest }, - secrets: mockOptions.secrets, - services, - }; - - handleIncidentMock.mockImplementation(() => incidentResponse); - - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse }); - }); - - test('should throw an error when failed to create incident', async () => { - expect.assertions(1); - const { incidentId, ...rest } = mockOptions.params; - - const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { ...rest }, - secrets: mockOptions.secrets, - services, - }; - const errorMessage = 'Failed to create incident'; - - handleIncidentMock.mockImplementation(() => { - throw new Error(errorMessage); - }); - - try { - await actionType.executor(executorOptions); - } catch (error) { - expect(error.message).toEqual(errorMessage); - } - }); - - test('should update an incident', async () => { - const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { - ...mockOptions.params, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - secrets: mockOptions.secrets, - services, - }; - - handleIncidentMock.mockImplementation(() => incidentResponse); - - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse }); - }); - - test('should throw an error when failed to update an incident', async () => { - expect.assertions(1); - - const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { - ...mockOptions.params, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - secrets: mockOptions.secrets, - services, - }; - const errorMessage = 'Failed to update incident'; - - handleIncidentMock.mockImplementation(() => { - throw new Error(errorMessage); - }); - - try { - await actionType.executor(executorOptions); - } catch (error) { - expect(error.message).toEqual(errorMessage); - } - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 5066190d4fe56..dbb536d2fa53d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -4,108 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { curry, isEmpty } from 'lodash'; -import { schema } from '@kbn/config-schema'; -import { - ActionType, - ActionTypeExecutorOptions, - ActionTypeExecutorResult, - ExecutorType, -} from '../../types'; -import { ActionsConfigurationUtilities } from '../../actions_config'; -import { ServiceNow } from './lib'; - -import * as i18n from './translations'; - -import { ACTION_TYPE_ID } from './constants'; -import { ConfigType, SecretsType, Comment, ExecutorParams } from './types'; - -import { ConfigSchemaProps, SecretsSchemaProps, ParamsSchema } from './schema'; - -import { buildMap, mapParams } from './helpers'; -import { handleIncident } from './action_handlers'; - -function validateConfig( - configurationUtilities: ActionsConfigurationUtilities, - configObject: ConfigType -) { - try { - if (isEmpty(configObject.casesConfiguration.mapping)) { - return i18n.MAPPING_EMPTY; - } - - configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); - } catch (whitelistError) { - return i18n.WHITE_LISTED_ERROR(whitelistError.message); - } -} - -function validateSecrets( - configurationUtilities: ActionsConfigurationUtilities, - secrets: SecretsType -) {} +import { createConnector } from '../case/utils'; -// action type definition -export function getActionType({ - configurationUtilities, - executor = serviceNowExecutor, -}: { - configurationUtilities: ActionsConfigurationUtilities; - executor?: ExecutorType; -}): ActionType { - return { - id: ACTION_TYPE_ID, - name: i18n.NAME, - minimumLicenseRequired: 'platinum', - validate: { - config: schema.object(ConfigSchemaProps, { - validate: curry(validateConfig)(configurationUtilities), - }), - secrets: schema.object(SecretsSchemaProps, { - validate: curry(validateSecrets)(configurationUtilities), - }), - params: ParamsSchema, - }, - executor, - }; -} - -// action executor - -async function serviceNowExecutor( - execOptions: ActionTypeExecutorOptions -): Promise { - const actionId = execOptions.actionId; - const { - apiUrl, - casesConfiguration: { mapping: configurationMapping }, - } = execOptions.config as ConfigType; - const { username, password } = execOptions.secrets as SecretsType; - const params = execOptions.params as ExecutorParams; - const { comments, incidentId, ...restParams } = params; - - const mapping = buildMap(configurationMapping); - const incident = mapParams((restParams as unknown) as Record, mapping); - const serviceNow = new ServiceNow({ url: apiUrl, username, password }); - - const handlerInput = { - incidentId, - serviceNow, - params: { ...params, incident }, - comments: comments as Comment[], - mapping, - }; - - const res: Pick & - Pick = { - status: 'ok', - actionId, - }; - - const data = await handleIncident(handlerInput); - - return { - ...res, - data, - }; -} +import { api } from './api'; +import { config } from './config'; +import { validate } from './validators'; +import { createExternalService } from './service'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, +} from '../case/schema'; + +export const getActionType = createConnector({ + api, + config, + validate, + createExternalService, + validationSchema: { + config: ExternalIncidentServiceConfiguration, + secrets: ExternalIncidentServiceSecretConfiguration, + }, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts deleted file mode 100644 index 3f102ae19f437..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts +++ /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. - */ - -export const API_VERSION = 'v2'; -export const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; -export const USER_URL = `api/now/${API_VERSION}/table/sys_user?user_name=`; -export const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; - -// Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html -export const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts deleted file mode 100644 index 40eeb0f920f82..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts +++ /dev/null @@ -1,334 +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 axios from 'axios'; -import { ServiceNow } from '.'; -import { instance, params } from '../mock'; - -jest.mock('axios'); - -axios.create = jest.fn(() => axios); -const axiosMock = (axios as unknown) as jest.Mock; - -let serviceNow: ServiceNow; - -const testMissingConfiguration = (field: string) => { - expect.assertions(1); - try { - new ServiceNow({ ...instance, [field]: '' }); - } catch (error) { - expect(error.message).toEqual('[Action][ServiceNow]: Wrong configuration.'); - } -}; - -const prependInstanceUrl = (url: string): string => `${instance.url}/${url}`; - -describe('ServiceNow lib', () => { - beforeEach(() => { - serviceNow = new ServiceNow(instance); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('should thrown an error if url is missing', () => { - testMissingConfiguration('url'); - }); - - test('should thrown an error if username is missing', () => { - testMissingConfiguration('username'); - }); - - test('should thrown an error if password is missing', () => { - testMissingConfiguration('password'); - }); - - test('get user id', async () => { - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: [{ sys_id: '123' }] }, - }); - - const res = await serviceNow.getUserID(); - const [url, { method }] = axiosMock.mock.calls[0]; - - expect(url).toEqual(prependInstanceUrl('api/now/v2/table/sys_user?user_name=username')); - expect(method).toEqual('get'); - expect(res).toEqual('123'); - }); - - test('create incident', async () => { - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_id: '123', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, - }); - - const res = await serviceNow.createIncident({ - short_description: 'A title', - description: 'A description', - caller_id: '123', - }); - const [url, { method, data }] = axiosMock.mock.calls[0]; - - expect(url).toEqual(prependInstanceUrl('api/now/v2/table/incident')); - expect(method).toEqual('post'); - expect(data).toEqual({ - short_description: 'A title', - description: 'A description', - caller_id: '123', - }); - - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('update incident', async () => { - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_id: '123', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, - }); - - const res = await serviceNow.updateIncident('123', { - short_description: params.title, - }); - const [url, { method, data }] = axiosMock.mock.calls[0]; - - expect(url).toEqual(prependInstanceUrl(`api/now/v2/table/incident/123`)); - expect(method).toEqual('patch'); - expect(data).toEqual({ short_description: params.title }); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('create comment', async () => { - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_updated_on: '2020-03-10 12:24:20' } }, - }); - - const comment = { - commentId: '456', - version: 'WzU3LDFd', - comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }; - - const res = await serviceNow.createComment('123', comment, 'comments'); - - const [url, { method, data }] = axiosMock.mock.calls[0]; - - expect(url).toEqual(prependInstanceUrl(`api/now/v2/table/incident/123`)); - expect(method).toEqual('patch'); - expect(data).toEqual({ - comments: 'A comment', - }); - - expect(res).toEqual({ - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }); - }); - - test('create batch comment', async () => { - axiosMock.mockReturnValueOnce({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_updated_on: '2020-03-10 12:24:20' } }, - }); - - axiosMock.mockReturnValueOnce({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_updated_on: '2020-03-10 12:25:20' } }, - }); - - const comments = [ - { - commentId: '123', - version: 'WzU3LDFd', - comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'A second comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]; - const res = await serviceNow.batchCreateComments('000', comments, 'comments'); - - comments.forEach((comment, index) => { - const [url, { method, data }] = axiosMock.mock.calls[index]; - expect(url).toEqual(prependInstanceUrl('api/now/v2/table/incident/000')); - expect(method).toEqual('patch'); - expect(data).toEqual({ - comments: comment.comment, - }); - expect(res).toEqual([ - { commentId: '123', pushedDate: '2020-03-10T12:24:20.000Z' }, - { commentId: '456', pushedDate: '2020-03-10T12:25:20.000Z' }, - ]); - }); - }); - - test('throw if not status is not ok', async () => { - expect.assertions(1); - - axiosMock.mockResolvedValue({ - status: 401, - headers: { - 'content-type': 'application/json', - }, - }); - try { - await serviceNow.getUserID(); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to get user id. Error: [ServiceNow]: Instance is not alive.' - ); - } - }); - - test('throw if not content-type is not application/json', async () => { - expect.assertions(1); - - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/html', - }, - }); - try { - await serviceNow.getUserID(); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to get user id. Error: [ServiceNow]: Instance is not alive.' - ); - } - }); - - test('check error when getting user', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.getUserID(); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to get user id. Error: Bad request.' - ); - } - }); - - test('check error when getting incident', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.getIncident('123'); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to get incident with id 123. Error: Bad request.' - ); - } - }); - - test('check error when creating incident', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.createIncident({ short_description: 'title' }); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to create incident. Error: Bad request.' - ); - } - }); - - test('check error when updating incident', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.updateIncident('123', { short_description: 'title' }); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to update incident with id 123. Error: Bad request.' - ); - } - }); - - test('check error when creating comment', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.createComment( - '123', - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'A second comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - 'comment' - ); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to create comment at incident with id 123. Error: Bad request.' - ); - } - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts deleted file mode 100644 index ed9cfe67a19a1..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts +++ /dev/null @@ -1,186 +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 axios, { AxiosInstance, Method, AxiosResponse } from 'axios'; - -import { INCIDENT_URL, USER_URL, COMMENT_URL, VIEW_INCIDENT_URL } from './constants'; -import { Instance, Incident, IncidentResponse, UpdateIncident, CommentResponse } from './types'; -import { Comment } from '../types'; - -const validStatusCodes = [200, 201]; - -class ServiceNow { - private readonly incidentUrl: string; - private readonly commentUrl: string; - private readonly userUrl: string; - private readonly axios: AxiosInstance; - - constructor(private readonly instance: Instance) { - if ( - !this.instance || - !this.instance.url || - !this.instance.username || - !this.instance.password - ) { - throw Error('[Action][ServiceNow]: Wrong configuration.'); - } - - this.incidentUrl = `${this.instance.url}/${INCIDENT_URL}`; - this.commentUrl = `${this.instance.url}/${COMMENT_URL}`; - this.userUrl = `${this.instance.url}/${USER_URL}`; - this.axios = axios.create({ - auth: { username: this.instance.username, password: this.instance.password }, - }); - } - - private _throwIfNotAlive(status: number, contentType: string) { - if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { - throw new Error('[ServiceNow]: Instance is not alive.'); - } - } - - private async _request({ - url, - method = 'get', - data = {}, - }: { - url: string; - method?: Method; - data?: unknown; - }): Promise { - const res = await this.axios(url, { method, data }); - this._throwIfNotAlive(res.status, res.headers['content-type']); - return res; - } - - private _patch({ url, data }: { url: string; data: unknown }): Promise { - return this._request({ - url, - method: 'patch', - data, - }); - } - - private _addTimeZoneToDate(date: string, timezone = 'GMT'): string { - return `${date} GMT`; - } - - private _getErrorMessage(msg: string) { - return `[Action][ServiceNow]: ${msg}`; - } - - private _getIncidentViewURL(id: string) { - return `${this.instance.url}/${VIEW_INCIDENT_URL}${id}`; - } - - async getUserID(): Promise { - try { - const res = await this._request({ url: `${this.userUrl}${this.instance.username}` }); - return res.data.result[0].sys_id; - } catch (error) { - throw new Error(this._getErrorMessage(`Unable to get user id. Error: ${error.message}`)); - } - } - - async getIncident(incidentId: string) { - try { - const res = await this._request({ - url: `${this.incidentUrl}/${incidentId}`, - }); - - return { ...res.data.result }; - } catch (error) { - throw new Error( - this._getErrorMessage( - `Unable to get incident with id ${incidentId}. Error: ${error.message}` - ) - ); - } - } - - async createIncident(incident: Incident): Promise { - try { - const res = await this._request({ - url: `${this.incidentUrl}`, - method: 'post', - data: { ...incident }, - }); - - return { - number: res.data.result.number, - incidentId: res.data.result.sys_id, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), - url: this._getIncidentViewURL(res.data.result.sys_id), - }; - } catch (error) { - throw new Error(this._getErrorMessage(`Unable to create incident. Error: ${error.message}`)); - } - } - - async updateIncident(incidentId: string, incident: UpdateIncident): Promise { - try { - const res = await this._patch({ - url: `${this.incidentUrl}/${incidentId}`, - data: { ...incident }, - }); - - return { - number: res.data.result.number, - incidentId: res.data.result.sys_id, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - url: this._getIncidentViewURL(res.data.result.sys_id), - }; - } catch (error) { - throw new Error( - this._getErrorMessage( - `Unable to update incident with id ${incidentId}. Error: ${error.message}` - ) - ); - } - } - - async batchCreateComments( - incidentId: string, - comments: Comment[], - field: string - ): Promise { - // Create comments sequentially. - const promises = comments.reduce(async (prevPromise, currentComment) => { - const totalComments = await prevPromise; - const res = await this.createComment(incidentId, currentComment, field); - return [...totalComments, res]; - }, Promise.resolve([] as CommentResponse[])); - - const res = await promises; - return res; - } - - async createComment( - incidentId: string, - comment: Comment, - field: string - ): Promise { - try { - const res = await this._patch({ - url: `${this.commentUrl}/${incidentId}`, - data: { [field]: comment.comment }, - }); - - return { - commentId: comment.commentId, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - }; - } catch (error) { - throw new Error( - this._getErrorMessage( - `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` - ) - ); - } - } -} - -export { ServiceNow }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts deleted file mode 100644 index a65e417dbc486..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts +++ /dev/null @@ -1,32 +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. - */ - -export interface Instance { - url: string; - username: string; - password: string; -} - -export interface Incident { - short_description: string; - description?: string; - caller_id?: string; - [index: string]: string | undefined; -} - -export interface IncidentResponse { - number: string; - incidentId: string; - pushedDate: string; - url: string; -} - -export interface CommentResponse { - commentId: string; - pushedDate: string; -} - -export type UpdateIncident = Partial; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts deleted file mode 100644 index 06c006fb37825..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts +++ /dev/null @@ -1,115 +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 { MapEntry, Mapping, ExecutorParams } from './types'; -import { Incident } from './lib/types'; - -const mapping: MapEntry[] = [ - { source: 'title', target: 'short_description', actionType: 'overwrite' }, - { source: 'description', target: 'description', actionType: 'append' }, - { source: 'comments', target: 'comments', actionType: 'append' }, -]; - -const finalMapping: Mapping = new Map(); - -finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', -}); - -finalMapping.set('description', { - target: 'description', - actionType: 'append', -}); - -finalMapping.set('comments', { - target: 'comments', - actionType: 'append', -}); - -finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', -}); - -const params: ExecutorParams = { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, - title: 'Incident title', - description: 'Incident description', - comments: [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, - }, - { - commentId: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d', - version: 'WlK3LDFd', - comment: 'Another comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, - }, - ], -}; - -const incidentResponse = { - incidentId: 'c816f79cc0a8016401c5a33be04be441', - number: 'INC0010001', - pushedDate: '2020-03-13T08:34:53.450Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', -}; - -const userId = '2e9a0a5e2f79001016ab51172799b670'; - -const axiosResponse = { - status: 200, - headers: { - 'content-type': 'application/json', - }, -}; -const userIdResponse = { - result: [{ sys_id: userId }], -}; - -const incidentAxiosResponse = { - result: { sys_id: incidentResponse.incidentId, number: incidentResponse.number }, -}; - -const instance = { - url: 'https://instance.service-now.com', - username: 'username', - password: 'password', -}; - -const incident: Incident = { - short_description: params.title, - description: params.description, - caller_id: userId, -}; - -export { - mapping, - finalMapping, - params, - incidentResponse, - incidentAxiosResponse, - userId, - userIdResponse, - axiosResponse, - instance, - incident, -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts new file mode 100644 index 0000000000000..37228380910b3 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts @@ -0,0 +1,117 @@ +/* + * 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 { + ExternalService, + PushToServiceApiParams, + ExecutorSubActionPushParams, + MapRecord, +} from '../case/types'; + +const createMock = (): jest.Mocked => { + const service = { + getIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + short_description: 'title from servicenow', + description: 'description from servicenow', + }) + ), + createIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }) + ), + updateIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }) + ), + createComment: jest.fn(), + }; + + service.createComment.mockImplementationOnce(() => + Promise.resolve({ + commentId: 'case-comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }) + ); + + service.createComment.mockImplementationOnce(() => + Promise.resolve({ + commentId: 'case-comment-2', + pushedDate: '2020-03-10T12:24:20.000Z', + }) + ); + return service; +}; + +const externalServiceMock = { + create: createMock, +}; + +const mapping: Map> = new Map(); + +mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', +}); + +mapping.set('description', { + target: 'description', + actionType: 'overwrite', +}); + +mapping.set('comments', { + target: 'comments', + actionType: 'append', +}); + +mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', +}); + +const executorParams: ExecutorSubActionPushParams = { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + externalId: 'incident-3', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + title: 'Incident title', + description: 'Incident description', + comments: [ + { + commentId: 'case-comment-1', + comment: 'A comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + }, + { + commentId: 'case-comment-2', + comment: 'Another comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + }, + ], +}; + +const apiParams: PushToServiceApiParams = { + ...executorParams, + externalCase: { short_description: 'Incident title', description: 'Incident description' }, +}; + +export { externalServiceMock, mapping, executorParams, apiParams }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts deleted file mode 100644 index 889b57c8e92e2..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ /dev/null @@ -1,70 +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'; - -export const MapEntrySchema = schema.object({ - source: schema.string(), - target: schema.string(), - actionType: schema.oneOf([ - schema.literal('nothing'), - schema.literal('overwrite'), - schema.literal('append'), - ]), -}); - -export const CasesConfigurationSchema = schema.object({ - mapping: schema.arrayOf(MapEntrySchema), -}); - -export const ConfigSchemaProps = { - apiUrl: schema.string(), - casesConfiguration: CasesConfigurationSchema, -}; - -export const ConfigSchema = schema.object(ConfigSchemaProps); - -export const SecretsSchemaProps = { - password: schema.string(), - username: schema.string(), -}; - -export const SecretsSchema = schema.object(SecretsSchemaProps); - -export const UserSchema = schema.object({ - fullName: schema.nullable(schema.string()), - username: schema.string(), -}); - -const EntityInformationSchemaProps = { - createdAt: schema.string(), - createdBy: UserSchema, - updatedAt: schema.nullable(schema.string()), - updatedBy: schema.nullable(UserSchema), -}; - -export const EntityInformationSchema = schema.object(EntityInformationSchemaProps); - -export const CommentSchema = schema.object({ - commentId: schema.string(), - comment: schema.string(), - version: schema.maybe(schema.string()), - ...EntityInformationSchemaProps, -}); - -export const ExecutorAction = schema.oneOf([ - schema.literal('newIncident'), - schema.literal('updateIncident'), -]); - -export const ParamsSchema = schema.object({ - caseId: schema.string(), - title: schema.string(), - comments: schema.maybe(schema.arrayOf(CommentSchema)), - description: schema.maybe(schema.string()), - incidentId: schema.nullable(schema.string()), - ...EntityInformationSchemaProps, -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts new file mode 100644 index 0000000000000..f65cd5430560e --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -0,0 +1,255 @@ +/* + * 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 axios from 'axios'; + +import { createExternalService } from './service'; +import * as utils from '../case/utils'; +import { ExternalService } from '../case/types'; + +jest.mock('axios'); +jest.mock('../case/utils', () => { + const originalUtils = jest.requireActual('../case/utils'); + return { + ...originalUtils, + request: jest.fn(), + patch: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; +const patchMock = utils.patch as jest.Mock; + +describe('ServiceNow service', () => { + let service: ExternalService; + + beforeAll(() => { + service = createExternalService({ + config: { apiUrl: 'https://dev102283.service-now.com' }, + secrets: { username: 'admin', password: 'admin' }, + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createExternalService', () => { + test('throws without url', () => { + expect(() => + createExternalService({ + config: { apiUrl: null }, + secrets: { username: 'admin', password: 'admin' }, + }) + ).toThrow(); + }); + + test('throws without username', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com' }, + secrets: { username: '', password: 'admin' }, + }) + ).toThrow(); + }); + + test('throws without password', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com' }, + secrets: { username: '', password: undefined }, + }) + ).toThrow(); + }); + }); + + describe('getIncident', () => { + test('it returns the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01' } }, + })); + const res = await service.getIncident('1'); + expect(res).toEqual({ sys_id: '1', number: 'INC01' }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01' } }, + })); + + await service.getIncident('1'); + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + expect(service.getIncident('1')).rejects.toThrow( + 'Unable to get incident with id 1. Error: An error has occurred' + ); + }); + }); + + describe('createIncident', () => { + test('it creates the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, + })); + + const res = await service.createIncident({ + incident: { short_description: 'title', description: 'desc' }, + }); + + expect(res).toEqual({ + title: 'INC01', + id: '1', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=1', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, + })); + + await service.createIncident({ + incident: { short_description: 'title', description: 'desc' }, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://dev102283.service-now.com/api/now/v2/table/incident', + method: 'post', + data: { short_description: 'title', description: 'desc' }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.createIncident({ + incident: { short_description: 'title', description: 'desc' }, + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to create incident. Error: An error has occurred' + ); + }); + }); + + describe('updateIncident', () => { + test('it updates the incident correctly', async () => { + patchMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + })); + + const res = await service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' }, + }); + + expect(res).toEqual({ + title: 'INC01', + id: '1', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=1', + }); + }); + + test('it should call request with correct arguments', async () => { + patchMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + })); + + await service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' }, + }); + + expect(patchMock).toHaveBeenCalledWith({ + axios, + url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', + data: { short_description: 'title', description: 'desc' }, + }); + }); + + test('it should throw an error', async () => { + patchMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' }, + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to update incident with id 1. Error: An error has occurred' + ); + }); + }); + + describe('createComment', () => { + test('it creates the comment correctly', async () => { + patchMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + })); + + const res = await service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'comments', + }); + + expect(res).toEqual({ + commentId: 'comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('it should call request with correct arguments', async () => { + patchMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + })); + + await service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'my_field', + }); + + expect(patchMock).toHaveBeenCalledWith({ + axios, + url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', + data: { my_field: 'comment' }, + }); + }); + + test('it should throw an error', async () => { + patchMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'comments', + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to create comment at incident with id 1. Error: An error has occurred' + ); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts new file mode 100644 index 0000000000000..541fefce2f2ff --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -0,0 +1,138 @@ +/* + * 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 axios from 'axios'; + +import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; +import { addTimeZoneToDate, patch, request, getErrorMessage } from '../case/utils'; + +import * as i18n from './translations'; +import { + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + CreateIncidentRequest, + UpdateIncidentRequest, + CreateCommentRequest, +} from './types'; + +const API_VERSION = 'v2'; +const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; +const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; + +// Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html +const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; + +export const createExternalService = ({ + config, + secrets, +}: ExternalServiceCredentials): ExternalService => { + const { apiUrl: url } = config as ServiceNowPublicConfigurationType; + const { username, password } = secrets as ServiceNowSecretConfigurationType; + + if (!url || !username || !password) { + throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); + } + + const incidentUrl = `${url}/${INCIDENT_URL}`; + const commentUrl = `${url}/${COMMENT_URL}`; + const axiosInstance = axios.create({ + auth: { username, password }, + }); + + const getIncidentViewURL = (id: string) => { + return `${url}/${VIEW_INCIDENT_URL}${id}`; + }; + + const getIncident = async (id: string) => { + try { + const res = await request({ + axios: axiosInstance, + url: `${incidentUrl}/${id}`, + }); + + return { ...res.data.result }; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`) + ); + } + }; + + const createIncident = async ({ incident }: ExternalServiceParams) => { + try { + const res = await request({ + axios: axiosInstance, + url: `${incidentUrl}`, + method: 'post', + data: { ...incident }, + }); + + return { + title: res.data.result.number, + id: res.data.result.sys_id, + pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), + url: getIncidentViewURL(res.data.result.sys_id), + }; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`) + ); + } + }; + + const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { + try { + const res = await patch({ + axios: axiosInstance, + url: `${incidentUrl}/${incidentId}`, + data: { ...incident }, + }); + + return { + title: res.data.result.number, + id: res.data.result.sys_id, + pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + url: getIncidentViewURL(res.data.result.sys_id), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to update incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } + }; + + const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { + try { + const res = await patch({ + axios: axiosInstance, + url: `${commentUrl}/${incidentId}`, + data: { [field]: comment.comment }, + }); + + return { + commentId: comment.commentId, + pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } + }; + + return { + getIncident, + createIncident, + updateIncident, + createComment, + }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts deleted file mode 100644 index dc0a03fab8c71..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts +++ /dev/null @@ -1,43 +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 { TransformerArgs } from './types'; -import * as i18n from './translations'; - -export const informationCreated = ({ - value, - date, - user, - ...rest -}: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`, - ...rest, -}); - -export const informationUpdated = ({ - value, - date, - user, - ...rest -}: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`, - ...rest, -}); - -export const informationAdded = ({ - value, - date, - user, - ...rest -}: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`, - ...rest, -}); - -export const append = ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ - value: previousValue ? `${previousValue} \r\n${value}` : `${value}`, - ...rest, -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index 3b216a6c3260a..3d6138169c4cc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -6,77 +6,6 @@ import { i18n } from '@kbn/i18n'; -export const API_URL_REQUIRED = i18n.translate( - 'xpack.actions.builtin.servicenow.servicenowApiNullError', - { - defaultMessage: 'ServiceNow [apiUrl] is required', - } -); - -export const WHITE_LISTED_ERROR = (message: string) => - i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', { - defaultMessage: 'error configuring servicenow action: {message}', - values: { - message, - }, - }); - -export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { +export const NAME = i18n.translate('xpack.actions.builtin.case.servicenowTitle', { defaultMessage: 'ServiceNow', }); - -export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.servicenow.emptyMapping', { - defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', -}); - -export const ERROR_POSTING = i18n.translate( - 'xpack.actions.builtin.servicenow.postingErrorMessage', - { - defaultMessage: 'error posting servicenow event', - } -); - -export const RETRY_POSTING = (status: number) => - i18n.translate('xpack.actions.builtin.servicenow.postingRetryErrorMessage', { - defaultMessage: 'error posting servicenow event: http status {status}, retry later', - values: { - status, - }, - }); - -export const UNEXPECTED_STATUS = (status: number) => - i18n.translate('xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage', { - defaultMessage: 'error posting servicenow event: unexpected status {status}', - values: { - status, - }, - }); - -export const FIELD_INFORMATION = ( - mode: string, - date: string | undefined, - user: string | undefined -) => { - switch (mode) { - case 'create': - return i18n.translate('xpack.actions.builtin.servicenow.informationCreated', { - values: { date, user }, - defaultMessage: '(created at {date} by {user})', - }); - case 'update': - return i18n.translate('xpack.actions.builtin.servicenow.informationUpdated', { - values: { date, user }, - defaultMessage: '(updated at {date} by {user})', - }); - case 'add': - return i18n.translate('xpack.actions.builtin.servicenow.informationAdded', { - values: { date, user }, - defaultMessage: '(added at {date} by {user})', - }); - default: - return i18n.translate('xpack.actions.builtin.servicenow.informationDefault', { - values: { date, user }, - defaultMessage: '(created at {date} by {user})', - }); - } -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index c5ef282aeffa7..d8476b7dca54a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -4,100 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TypeOf } from '@kbn/config-schema'; +export { + ExternalIncidentServiceConfiguration as ServiceNowPublicConfigurationType, + ExternalIncidentServiceSecretConfiguration as ServiceNowSecretConfigurationType, +} from '../case/types'; -import { - ConfigSchema, - SecretsSchema, - ParamsSchema, - CasesConfigurationSchema, - MapEntrySchema, - CommentSchema, -} from './schema'; - -import { ServiceNow } from './lib'; -import { Incident, IncidentResponse } from './lib/types'; - -// config definition -export type ConfigType = TypeOf; - -// secrets definition -export type SecretsType = TypeOf; - -export type ExecutorParams = TypeOf; - -export type CasesConfigurationType = TypeOf; -export type MapEntry = TypeOf; -export type Comment = TypeOf; - -export type Mapping = Map>; - -export interface Params extends ExecutorParams { - incident: Record; +export interface CreateIncidentRequest { + summary: string; + description: string; } -export interface CreateHandlerArguments { - serviceNow: ServiceNow; - params: Params; - comments: Comment[]; - mapping: Mapping; -} - -export type UpdateHandlerArguments = CreateHandlerArguments & { - incidentId: string; -}; - -export type IncidentHandlerArguments = CreateHandlerArguments & { - incidentId: string | null; -}; -export interface HandlerResponse extends IncidentResponse { - comments?: SimpleComment[]; -} - -export interface SimpleComment { - commentId: string; - pushedDate: string; -} - -export interface AppendFieldArgs { - value: string; - prefix?: string; - suffix?: string; -} - -export interface KeyAny { - [index: string]: unknown; -} - -export interface AppendInformationFieldArgs { - value: string; - user: string; - date: string; - mode: string; -} - -export interface TransformerArgs { - value: string; - date?: string; - user?: string; - previousValue?: string; -} - -export interface PrepareFieldsForTransformArgs { - params: Params; - mapping: Mapping; - defaultPipes?: string[]; -} - -export interface PipedField { - key: string; - value: string; - actionType: string; - pipes: string[]; -} +export type UpdateIncidentRequest = Partial; -export interface TransformFieldsArgs { - params: Params; - fields: PipedField[]; - currentIncident?: Incident; +export interface CreateCommentRequest { + [key: string]: string; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts new file mode 100644 index 0000000000000..7226071392bc6 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.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 { validateCommonConfig, validateCommonSecrets } from '../case/validators'; +import { ExternalServiceValidation } from '../case/types'; + +export const validate: ExternalServiceValidation = { + config: validateCommonConfig, + secrets: validateCommonSecrets, +}; diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index 1f08a41024905..d1bcae549805e 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -127,35 +127,34 @@ export const ServiceConnectorCommentParamsRt = rt.type({ updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), }); -export const ServiceConnectorCaseParamsRt = rt.intersection([ - rt.type({ - caseId: rt.string, - createdAt: rt.string, - createdBy: ServiceConnectorUserParams, - incidentId: rt.union([rt.string, rt.null]), - title: rt.string, - updatedAt: rt.union([rt.string, rt.null]), - updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), - }), - rt.partial({ - description: rt.string, - comments: rt.array(ServiceConnectorCommentParamsRt), - }), -]); +export const ServiceConnectorCaseParamsRt = rt.type({ + caseId: rt.string, + createdAt: rt.string, + createdBy: ServiceConnectorUserParams, + externalId: rt.union([rt.string, rt.null]), + title: rt.string, + updatedAt: rt.union([rt.string, rt.null]), + updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), + description: rt.union([rt.string, rt.null]), + comments: rt.union([rt.array(ServiceConnectorCommentParamsRt), rt.null]), +}); export const ServiceConnectorCaseResponseRt = rt.intersection([ rt.type({ - number: rt.string, - incidentId: rt.string, + title: rt.string, + id: rt.string, pushedDate: rt.string, url: rt.string, }), rt.partial({ comments: rt.array( - rt.type({ - commentId: rt.string, - pushedDate: rt.string, - }) + rt.intersection([ + rt.type({ + commentId: rt.string, + pushedDate: rt.string, + }), + rt.partial({ externalCommentId: rt.string }), + ]) ), }), ]); diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index dcfa46bfa6019..855a5c3d63507 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -27,3 +27,5 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; export const ACTION_URL = '/api/action'; export const ACTION_TYPES_URL = '/api/action/types'; + +export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira']; diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index 00575655d4c42..43167d56de015 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -8,14 +8,15 @@ import Boom from 'boom'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants'; +import { + CASE_CONFIGURE_CONNECTORS_URL, + SUPPORTED_CONNECTORS, +} from '../../../../../common/constants'; /* * Be aware that this api will only return 20 connectors */ -const CASE_SERVICE_NOW_ACTION = '.servicenow'; - export function initCaseConfigureGetActionConnector({ caseService, router }: RouteDeps) { router.get( { @@ -30,8 +31,8 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou throw Boom.notFound('Action client have not been found'); } - const results = (await actionsClient.getAll()).filter( - action => action.actionTypeId === CASE_SERVICE_NOW_ACTION + const results = (await actionsClient.getAll()).filter(action => + SUPPORTED_CONNECTORS.includes(action.actionTypeId) ); return response.ok({ body: results }); } catch (error) { diff --git a/x-pack/plugins/siem/public/containers/case/api.test.tsx b/x-pack/plugins/siem/public/containers/case/api.test.tsx index ad61e2b46f6c5..174738098fa10 100644 --- a/x-pack/plugins/siem/public/containers/case/api.test.tsx +++ b/x-pack/plugins/siem/public/containers/case/api.test.tsx @@ -418,7 +418,9 @@ describe('Case Configuration API', () => { await pushToService(connectorId, casePushParams, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`/api/action/${connectorId}/_execute`, { method: 'POST', - body: JSON.stringify({ params: casePushParams }), + body: JSON.stringify({ + params: { subAction: 'pushToService', subActionParams: casePushParams }, + }), signal: abortCtrl.signal, }); }); diff --git a/x-pack/plugins/siem/public/containers/case/api.ts b/x-pack/plugins/siem/public/containers/case/api.ts index b97f94a5a6b59..438eae9d88a44 100644 --- a/x-pack/plugins/siem/public/containers/case/api.ts +++ b/x-pack/plugins/siem/public/containers/case/api.ts @@ -245,7 +245,9 @@ export const pushToService = async ( `${ACTION_URL}/${connectorId}/_execute`, { method: 'POST', - body: JSON.stringify({ params: casePushParams }), + body: JSON.stringify({ + params: { subAction: 'pushToService', subActionParams: casePushParams }, + }), signal, } ); diff --git a/x-pack/plugins/siem/public/containers/case/mock.ts b/x-pack/plugins/siem/public/containers/case/mock.ts index 0f44b3a1594ba..a3a8db2c40950 100644 --- a/x-pack/plugins/siem/public/containers/case/mock.ts +++ b/x-pack/plugins/siem/public/containers/case/mock.ts @@ -103,8 +103,8 @@ export const pushedCase: Case = { }; export const serviceConnector: ServiceConnectorCaseResponse = { - number: '123', - incidentId: '444', + title: '123', + id: '444', pushedDate: basicUpdatedAt, url: 'connector.com', comments: [ @@ -129,12 +129,13 @@ export const casePushParams = { caseId: basicCaseId, createdAt: basicCreatedAt, createdBy: elasticUser, - incidentId: null, + externalId: null, title: 'what a cool value', commentId: null, updatedAt: basicCreatedAt, updatedBy: elasticUser, description: 'nice', + comments: null, }; export const actionTypeExecutorResult = { actionId: 'string', diff --git a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx index b07a346a8da46..b9698c3e864e3 100644 --- a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx @@ -55,8 +55,8 @@ describe('usePostPushToService', () => { { connector_id: samplePush.connectorId, connector_name: samplePush.connectorName, - external_id: serviceConnector.incidentId, - external_title: serviceConnector.number, + external_id: serviceConnector.id, + external_title: serviceConnector.title, external_url: serviceConnector.url, }, abortCtrl.signal diff --git a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx index acd4b92ee430d..c9d1b963f411a 100644 --- a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx @@ -98,8 +98,8 @@ export const usePostPushToService = (): UsePostPushToService => { { connector_id: connectorId, connector_name: connectorName, - external_id: responseService.incidentId, - external_title: responseService.number, + external_id: responseService.id, + external_title: responseService.title, external_url: responseService.url, }, abortCtrl.signal @@ -180,7 +180,7 @@ export const formatServiceRequestData = (myCase: Case): ServiceConnectorCasePara : null, })), description, - incidentId: externalService?.externalId ?? null, + externalId: externalService?.externalId ?? null, title, updatedAt, updatedBy: diff --git a/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx new file mode 100644 index 0000000000000..c5a35da56284d --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx @@ -0,0 +1,148 @@ +/* + * 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, { useCallback, useEffect } from 'react'; +import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; + +import { isEmpty, get } from 'lodash/fp'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionConnectorFieldsProps } from '../../../../../../triggers_actions_ui/public/types'; +import { FieldMapping } from '../../../../pages/case/components/configure_cases/field_mapping'; + +import { defaultMapping } from '../../config'; +import { CasesConfigurationMapping } from '../../../../containers/case/configure/types'; + +import * as i18n from '../../translations'; +import { ActionConnector, ConnectorFlyoutHOCProps } from '../../types'; + +export const withConnectorFlyout = ({ + ConnectorFormComponent, + secretKeys = [], + configKeys = [], +}: ConnectorFlyoutHOCProps) => { + const ConnectorFlyout: React.FC> = ({ + action, + editActionConfig, + editActionSecrets, + errors, + }) => { + /* We do not provide defaults values to the fields (like empty string for apiUrl) intentionally. + * If we do, errors will be shown the first time the flyout is open even though the user did not + * interact with the form. Also, we would like to show errors for empty fields provided by the user. + /*/ + const { apiUrl, casesConfiguration: { mapping = [] } = {} } = action.config; + const configKeysWithDefault = [...configKeys, 'apiUrl']; + + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; + + /** + * We need to distinguish between the add flyout and the edit flyout. + * useEffect will run only once on component mount. + * This guarantees that the function below will run only once. + * On the first render of the component the apiUrl can be either undefined or filled. + * If it is filled then we are on the edit flyout. Otherwise we are on the add flyout. + */ + + useEffect(() => { + if (!isEmpty(apiUrl)) { + secretKeys.forEach((key: string) => editActionSecrets(key, '')); + } + }, []); + + if (isEmpty(mapping)) { + editActionConfig('casesConfiguration', { + ...action.config.casesConfiguration, + mapping: defaultMapping, + }); + } + + const handleOnChangeActionConfig = useCallback( + (key: string, value: string) => editActionConfig(key, value), + [] + ); + + const handleOnBlurActionConfig = useCallback( + (key: string) => { + if (configKeysWithDefault.includes(key) && get(key, action.config) == null) { + editActionConfig(key, ''); + } + }, + [action.config] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, value: string) => editActionSecrets(key, value), + [] + ); + + const handleOnBlurSecretConfig = useCallback( + (key: string) => { + if (secretKeys.includes(key) && get(key, action.secrets) == null) { + editActionSecrets(key, ''); + } + }, + [action.secrets] + ); + + const handleOnChangeMappingConfig = useCallback( + (newMapping: CasesConfigurationMapping[]) => + editActionConfig('casesConfiguration', { + ...action.config.casesConfiguration, + mapping: newMapping, + }), + [action.config] + ); + + return ( + <> + + + + handleOnChangeActionConfig('apiUrl', evt.target.value)} + onBlur={handleOnBlurActionConfig.bind(null, 'apiUrl')} + /> + + + + + + + + + + + + + ); + }; + + return ConnectorFlyout; +}; diff --git a/x-pack/plugins/siem/public/lib/connectors/config.ts b/x-pack/plugins/siem/public/lib/connectors/config.ts index baeb69b3f6943..98473e49622a9 100644 --- a/x-pack/plugins/siem/public/lib/connectors/config.ts +++ b/x-pack/plugins/siem/public/lib/connectors/config.ts @@ -5,17 +5,17 @@ */ import { CasesConfigurationMapping } from '../../containers/case/configure/types'; -import serviceNowLogo from './logos/servicenow.svg'; + import { Connector } from './types'; +import { connector as serviceNowConnectorConfig } from './servicenow/config'; +import { connector as jiraConnectorConfig } from './jira/config'; -const connectors: Record = { - '.servicenow': { - actionTypeId: '.servicenow', - logo: serviceNowLogo, - }, +export const connectorsConfiguration: Record = { + '.servicenow': serviceNowConnectorConfig, + '.jira': jiraConnectorConfig, }; -const defaultMapping: CasesConfigurationMapping[] = [ +export const defaultMapping: CasesConfigurationMapping[] = [ { source: 'title', target: 'short_description', @@ -32,5 +32,3 @@ const defaultMapping: CasesConfigurationMapping[] = [ actionType: 'append', }, ]; - -export { connectors, defaultMapping }; diff --git a/x-pack/plugins/siem/public/lib/connectors/index.ts b/x-pack/plugins/siem/public/lib/connectors/index.ts index fdf337b5ef120..2ce61bef49c5e 100644 --- a/x-pack/plugins/siem/public/lib/connectors/index.ts +++ b/x-pack/plugins/siem/public/lib/connectors/index.ts @@ -5,3 +5,4 @@ */ export { getActionType as serviceNowActionType } from './servicenow'; +export { getActionType as jiraActionType } from './jira'; diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/config.ts b/x-pack/plugins/siem/public/lib/connectors/jira/config.ts new file mode 100644 index 0000000000000..42bd1b9cdc191 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/config.ts @@ -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 { Connector } from '../types'; + +import { JIRA_TITLE } from './translations'; +import logo from './logo.svg'; + +export const connector: Connector = { + id: '.jira', + name: JIRA_TITLE, + logo, + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', +}; diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx new file mode 100644 index 0000000000000..482808fca53b1 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx @@ -0,0 +1,110 @@ +/* + * 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 { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldPassword, + EuiSpacer, +} from '@elastic/eui'; + +import * as i18n from './translations'; +import { ConnectorFlyoutFormProps } from '../types'; +import { JiraActionConnector } from './types'; +import { withConnectorFlyout } from '../components/connector_flyout'; + +const JiraConnectorForm: React.FC> = ({ + errors, + action, + onChangeSecret, + onBlurSecret, + onChangeConfig, + onBlurConfig, +}) => { + const { projectKey } = action.config; + const { email, apiToken } = action.secrets; + const isProjectKeyInvalid: boolean = errors.projectKey.length > 0 && projectKey != null; + const isEmailInvalid: boolean = errors.email.length > 0 && email != null; + const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken != null; + + return ( + <> + + + + onChangeConfig('projectKey', evt.target.value)} + onBlur={() => onBlurConfig('projectKey')} + /> + + + + + + + + onChangeSecret('email', evt.target.value)} + onBlur={() => onBlurSecret('email')} + /> + + + + + + + + onChangeSecret('apiToken', evt.target.value)} + onBlur={() => onBlurSecret('apiToken')} + /> + + + + + ); +}; + +export const JiraConnectorFlyout = withConnectorFlyout({ + ConnectorFormComponent: JiraConnectorForm, + secretKeys: ['email', 'apiToken'], + configKeys: ['projectKey'], +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx new file mode 100644 index 0000000000000..ada9608e37c98 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx @@ -0,0 +1,54 @@ +/* + * 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 { + ValidationResult, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/types'; + +import { connector } from './config'; +import { createActionType } from '../utils'; +import logo from './logo.svg'; +import { JiraActionConnector } from './types'; +import { JiraConnectorFlyout } from './flyout'; +import * as i18n from './translations'; + +interface Errors { + projectKey: string[]; + email: string[]; + apiToken: string[]; +} + +const validateConnector = (action: JiraActionConnector): ValidationResult => { + const errors: Errors = { + projectKey: [], + email: [], + apiToken: [], + }; + + if (!action.config.projectKey) { + errors.projectKey = [...errors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED]; + } + + if (!action.secrets.email) { + errors.email = [...errors.email, i18n.EMAIL_REQUIRED]; + } + + if (!action.secrets.apiToken) { + errors.apiToken = [...errors.apiToken, i18n.API_TOKEN_REQUIRED]; + } + + return { errors }; +}; + +export const getActionType = createActionType({ + id: connector.id, + iconClass: logo, + selectMessage: i18n.JIRA_DESC, + actionTypeTitle: connector.name, + validateConnector, + actionConnectorFields: JiraConnectorFlyout, +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/logos/servicenow.svg b/x-pack/plugins/siem/public/lib/connectors/jira/logo.svg old mode 100755 new mode 100644 similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/logos/servicenow.svg rename to x-pack/plugins/siem/public/lib/connectors/jira/logo.svg diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts b/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts new file mode 100644 index 0000000000000..751aaecdad964 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts @@ -0,0 +1,28 @@ +/* + * 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 * from '../translations'; + +export const JIRA_DESC = i18n.translate('xpack.siem.case.connectors.jira.selectMessageText', { + defaultMessage: 'Push or update SIEM case data to a new issue in Jira', +}); + +export const JIRA_TITLE = i18n.translate('xpack.siem.case.connectors.jira.actionTypeTitle', { + defaultMessage: 'Jira', +}); + +export const JIRA_PROJECT_KEY_LABEL = i18n.translate('xpack.siem.case.connectors.jira.projectKey', { + defaultMessage: 'Project key', +}); + +export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.jira.requiredProjectKeyTextField', + { + defaultMessage: 'Project key is required', + } +); diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/types.ts b/x-pack/plugins/siem/public/lib/connectors/jira/types.ts new file mode 100644 index 0000000000000..13e4e8f6a289e --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/types.ts @@ -0,0 +1,18 @@ +/* + * 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-restricted-imports */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { + JiraPublicConfigurationType, + JiraSecretConfigurationType, +} from '../../../../../actions/server/builtin_action_types/jira/types'; + +export interface JiraActionConnector { + config: JiraPublicConfigurationType; + secrets: JiraSecretConfigurationType; +} diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow.tsx deleted file mode 100644 index 9fe0b4a957ceb..0000000000000 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow.tsx +++ /dev/null @@ -1,246 +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, { useCallback, ChangeEvent, useEffect } from 'react'; -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, -} from '@elastic/eui'; - -import { isEmpty, get } from 'lodash/fp'; - -import { - ActionConnectorFieldsProps, - ActionTypeModel, - ValidationResult, - ActionParamsProps, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../triggers_actions_ui/public/types'; - -import { FieldMapping } from '../../pages/case/components/configure_cases/field_mapping'; - -import * as i18n from './translations'; - -import { ServiceNowActionConnector } from './types'; -import { isUrlInvalid } from './validators'; - -import { connectors, defaultMapping } from './config'; -import { CasesConfigurationMapping } from '../../containers/case/configure/types'; - -const serviceNowDefinition = connectors['.servicenow']; - -interface ServiceNowActionParams { - message: string; -} - -interface Errors { - apiUrl: string[]; - username: string[]; - password: string[]; -} - -export function getActionType(): ActionTypeModel { - return { - id: serviceNowDefinition.actionTypeId, - iconClass: serviceNowDefinition.logo, - selectMessage: i18n.SERVICENOW_DESC, - actionTypeTitle: i18n.SERVICENOW_TITLE, - validateConnector: (action: ServiceNowActionConnector): ValidationResult => { - const errors: Errors = { - apiUrl: [], - username: [], - password: [], - }; - - if (!action.config.apiUrl) { - errors.apiUrl = [...errors.apiUrl, i18n.SERVICENOW_API_URL_REQUIRED]; - } - - if (isUrlInvalid(action.config.apiUrl)) { - errors.apiUrl = [...errors.apiUrl, i18n.SERVICENOW_API_URL_INVALID]; - } - - if (!action.secrets.username) { - errors.username = [...errors.username, i18n.SERVICENOW_USERNAME_REQUIRED]; - } - - if (!action.secrets.password) { - errors.password = [...errors.password, i18n.SERVICENOW_PASSWORD_REQUIRED]; - } - - return { errors }; - }, - validateParams: (actionParams: ServiceNowActionParams): ValidationResult => { - return { errors: {} }; - }, - actionConnectorFields: ServiceNowConnectorFields, - actionParamsFields: ServiceNowParamsFields, - }; -} - -const ServiceNowConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, editActionSecrets, errors }) => { - /* We do not provide defaults values to the fields (like empty string for apiUrl) intentionally. - * If we do, errors will be shown the first time the flyout is open even though the user did not - * interact with the form. Also, we would like to show errors for empty fields provided by the user. - /*/ - const { apiUrl, casesConfiguration: { mapping = [] } = {} } = action.config; - const { username, password } = action.secrets; - - const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; - const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; - const isPasswordInvalid: boolean = errors.password.length > 0 && password != null; - - /** - * We need to distinguish between the add flyout and the edit flyout. - * useEffect will run only once on component mount. - * This guarantees that the function below will run only once. - * On the first render of the component the apiUrl can be either undefined or filled. - * If it is filled then we are on the edit flyout. Otherwise we are on the add flyout. - */ - - useEffect(() => { - if (!isEmpty(apiUrl)) { - editActionSecrets('username', ''); - editActionSecrets('password', ''); - } - }, []); - - if (isEmpty(mapping)) { - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: defaultMapping, - }); - } - - const handleOnChangeActionConfig = useCallback( - (key: string, evt: ChangeEvent) => editActionConfig(key, evt.target.value), - [] - ); - - const handleOnBlurActionConfig = useCallback( - (key: string) => { - if (key === 'apiUrl' && action.config[key] == null) { - editActionConfig(key, ''); - } - }, - [action.config] - ); - - const handleOnChangeSecretConfig = useCallback( - (key: string, evt: ChangeEvent) => editActionSecrets(key, evt.target.value), - [] - ); - - const handleOnBlurSecretConfig = useCallback( - (key: string) => { - if (['username', 'password'].includes(key) && get(key, action.secrets) == null) { - editActionSecrets(key, ''); - } - }, - [action.secrets] - ); - - const handleOnChangeMappingConfig = useCallback( - (newMapping: CasesConfigurationMapping[]) => - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: newMapping, - }), - [action.config] - ); - - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -const ServiceNowParamsFields: React.FunctionComponent> = ({ actionParams, editAction, index, errors }) => { - return null; -}; diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts new file mode 100644 index 0000000000000..7bc1b117b3422 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts @@ -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 { Connector } from '../types'; + +import { SERVICENOW_TITLE } from './translations'; +import logo from './logo.svg'; + +export const connector: Connector = { + id: '.servicenow', + name: SERVICENOW_TITLE, + logo, + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', +}; diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx new file mode 100644 index 0000000000000..bcde802e7bd1e --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx @@ -0,0 +1,83 @@ +/* + * 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 { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldPassword, + EuiSpacer, +} from '@elastic/eui'; + +import * as i18n from './translations'; +import { ConnectorFlyoutFormProps } from '../types'; +import { ServiceNowActionConnector } from './types'; +import { withConnectorFlyout } from '../components/connector_flyout'; + +const ServiceNowConnectorForm: React.FC> = ({ + errors, + action, + onChangeSecret, + onBlurSecret, +}) => { + const { username, password } = action.secrets; + const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; + const isPasswordInvalid: boolean = errors.password.length > 0 && password != null; + + return ( + <> + + + + onChangeSecret('username', evt.target.value)} + onBlur={() => onBlurSecret('username')} + /> + + + + + + + + onChangeSecret('password', evt.target.value)} + onBlur={() => onBlurSecret('password')} + /> + + + + + ); +}; + +export const ServiceNowConnectorFlyout = withConnectorFlyout({ + ConnectorFormComponent: ServiceNowConnectorForm, + secretKeys: ['username', 'password'], +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx new file mode 100644 index 0000000000000..1f8e61b6d3ea7 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx @@ -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 { + ValidationResult, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/types'; + +import { connector } from './config'; +import { createActionType } from '../utils'; +import logo from './logo.svg'; +import { ServiceNowActionConnector } from './types'; +import { ServiceNowConnectorFlyout } from './flyout'; +import * as i18n from './translations'; + +interface Errors { + username: string[]; + password: string[]; +} + +const validateConnector = (action: ServiceNowActionConnector): ValidationResult => { + const errors: Errors = { + username: [], + password: [], + }; + + if (!action.secrets.username) { + errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; + } + + if (!action.secrets.password) { + errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; + } + + return { errors }; +}; + +export const getActionType = createActionType({ + id: connector.id, + iconClass: logo, + selectMessage: i18n.SERVICENOW_DESC, + actionTypeTitle: connector.name, + validateConnector, + actionConnectorFields: ServiceNowConnectorFlyout, +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg b/x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg new file mode 100644 index 0000000000000..dcd022a8dca18 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts new file mode 100644 index 0000000000000..5dac9eddd1536 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts @@ -0,0 +1,23 @@ +/* + * 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 * from '../translations'; + +export const SERVICENOW_DESC = i18n.translate( + 'xpack.siem.case.connectors.servicenow.selectMessageText', + { + defaultMessage: 'Push or update SIEM case data to a new incident in ServiceNow', + } +); + +export const SERVICENOW_TITLE = i18n.translate( + 'xpack.siem.case.connectors.servicenow.actionTypeTitle', + { + defaultMessage: 'ServiceNow', + } +); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts new file mode 100644 index 0000000000000..b7f0e79eb37e3 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts @@ -0,0 +1,18 @@ +/* + * 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-restricted-imports */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, +} from '../../../../../actions/server/builtin_action_types/servicenow/types'; + +export interface ServiceNowActionConnector { + config: ServiceNowPublicConfigurationType; + secrets: ServiceNowSecretConfigurationType; +} diff --git a/x-pack/plugins/siem/public/lib/connectors/translations.ts b/x-pack/plugins/siem/public/lib/connectors/translations.ts index ae2084120255c..b9c1d0fa2a17f 100644 --- a/x-pack/plugins/siem/public/lib/connectors/translations.ts +++ b/x-pack/plugins/siem/public/lib/connectors/translations.ts @@ -6,65 +6,76 @@ import { i18n } from '@kbn/i18n'; -export const SERVICENOW_DESC = i18n.translate( - 'xpack.siem.case.connectors.servicenow.selectMessageText', +export const API_URL_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.apiUrlTextFieldLabel', { - defaultMessage: 'Push or update SIEM case data to a new incident in ServiceNow', + defaultMessage: 'URL', } ); -export const SERVICENOW_TITLE = i18n.translate( - 'xpack.siem.case.connectors.servicenow.actionTypeTitle', +export const API_URL_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredApiUrlTextField', { - defaultMessage: 'ServiceNow', + defaultMessage: 'URL is required', } ); -export const SERVICENOW_API_URL_LABEL = i18n.translate( - 'xpack.siem.case.connectors.servicenow.apiUrlTextFieldLabel', +export const API_URL_INVALID = i18n.translate( + 'xpack.siem.case.connectors.common.invalidApiUrlTextField', { - defaultMessage: 'URL', + defaultMessage: 'URL is invalid', } ); -export const SERVICENOW_API_URL_REQUIRED = i18n.translate( - 'xpack.siem.case.connectors.servicenow.requiredApiUrlTextField', +export const USERNAME_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.usernameTextFieldLabel', { - defaultMessage: 'URL is required', + defaultMessage: 'Username', } ); -export const SERVICENOW_API_URL_INVALID = i18n.translate( - 'xpack.siem.case.connectors.servicenow.invalidApiUrlTextField', +export const USERNAME_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredUsernameTextField', { - defaultMessage: 'URL is invalid', + defaultMessage: 'Username is required', } ); -export const SERVICENOW_USERNAME_LABEL = i18n.translate( - 'xpack.siem.case.connectors.servicenow.usernameTextFieldLabel', +export const PASSWORD_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.passwordTextFieldLabel', { - defaultMessage: 'Username', + defaultMessage: 'Password', } ); -export const SERVICENOW_USERNAME_REQUIRED = i18n.translate( - 'xpack.siem.case.connectors.servicenow.requiredUsernameTextField', +export const PASSWORD_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredPasswordTextField', { - defaultMessage: 'Username is required', + defaultMessage: 'Password is required', } ); -export const SERVICENOW_PASSWORD_LABEL = i18n.translate( - 'xpack.siem.case.connectors.servicenow.passwordTextFieldLabel', +export const API_TOKEN_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.apiTokenTextFieldLabel', { - defaultMessage: 'Password', + defaultMessage: 'Api token', } ); -export const SERVICENOW_PASSWORD_REQUIRED = i18n.translate( - 'xpack.siem.case.connectors.servicenow.requiredPasswordTextField', +export const API_TOKEN_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredApiTokenTextField', { - defaultMessage: 'Password is required', + defaultMessage: 'Api token is required', + } +); + +export const EMAIL_LABEL = i18n.translate('xpack.siem.case.connectors.common.emailTextFieldLabel', { + defaultMessage: 'Email', +}); + +export const EMAIL_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredEmailTextField', + { + defaultMessage: 'Email is required', } ); diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts index 2def4b5107aee..9af60f4995e54 100644 --- a/x-pack/plugins/siem/public/lib/connectors/types.ts +++ b/x-pack/plugins/siem/public/lib/connectors/types.ts @@ -7,17 +7,39 @@ /* eslint-disable no-restricted-imports */ /* eslint-disable @kbn/eslint/no-restricted-paths */ -import { - ConfigType, - SecretsType, -} from '../../../../actions/server/builtin_action_types/servicenow/types'; - -export interface ServiceNowActionConnector { - config: ConfigType; - secrets: SecretsType; -} +import { ActionType } from '../../../../triggers_actions_ui/public'; +import { ExternalIncidentServiceConfiguration } from '../../../../actions/server/builtin_action_types/case/types'; -export interface Connector { - actionTypeId: string; +export interface Connector extends ActionType { logo: string; } + +export interface ActionConnector { + config: ExternalIncidentServiceConfiguration; + secrets: {}; +} + +export interface ActionConnectorParams { + message: string; +} + +export interface ActionConnectorValidationErrors { + apiUrl: string[]; +} + +export type Optional = Omit & Partial; + +export interface ConnectorFlyoutFormProps { + errors: { [key: string]: string[] }; + action: T; + onChangeSecret: (key: string, value: string) => void; + onBlurSecret: (key: string) => void; + onChangeConfig: (key: string, value: string) => void; + onBlurConfig: (key: string) => void; +} + +export interface ConnectorFlyoutHOCProps { + ConnectorFormComponent: React.FC>; + configKeys?: string[]; + secretKeys?: string[]; +} diff --git a/x-pack/plugins/siem/public/lib/connectors/utils.ts b/x-pack/plugins/siem/public/lib/connectors/utils.ts new file mode 100644 index 0000000000000..5b5270ade5a65 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/utils.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 { + ActionTypeModel, + ValidationResult, + ActionParamsProps, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../triggers_actions_ui/public/types'; + +import { + ActionConnector, + ActionConnectorParams, + ActionConnectorValidationErrors, + Optional, +} from './types'; +import { isUrlInvalid } from './validators'; + +import * as i18n from './translations'; + +export const createActionType = ({ + id, + actionTypeTitle, + selectMessage, + iconClass, + validateConnector, + validateParams = connectorParamsValidator, + actionConnectorFields, + actionParamsFields = ConnectorParamsFields, +}: Optional) => (): ActionTypeModel => { + return { + id, + iconClass, + selectMessage, + actionTypeTitle, + validateConnector: (action: ActionConnector): ValidationResult => { + const errors: ActionConnectorValidationErrors = { + apiUrl: [], + }; + + if (!action.config.apiUrl) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; + } + + if (isUrlInvalid(action.config.apiUrl)) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; + } + + return { errors: { ...errors, ...validateConnector(action).errors } }; + }, + validateParams, + actionConnectorFields, + actionParamsFields, + }; +}; + +const ConnectorParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, +}) => { + return null; +}; + +const connectorParamsValidator = (actionParams: ActionConnectorParams): ValidationResult => { + return { errors: {} }; +}; diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx index 15066e73eee82..d5575f3bac4c8 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx @@ -9,7 +9,7 @@ import { EuiIcon, EuiSuperSelect } from '@elastic/eui'; import styled from 'styled-components'; import { Connector } from '../../../../containers/case/configure/types'; -import { connectors as connectorsDefinition } from '../../../../lib/connectors/config'; +import { connectorsConfiguration } from '../../../../lib/connectors/config'; import * as i18n from './translations'; export interface Props { @@ -54,7 +54,7 @@ const ConnectorsDropdownComponent: React.FC = ({ inputDisplay: ( <> {connector.name} diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx index 545eceb0f73a1..fde179f3d25fc 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx @@ -186,6 +186,16 @@ describe('ConfigureCases', () => { id: '.servicenow', name: 'ServiceNow', enabled: true, + logo: 'test-file-stub', + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', + }, + { + id: '.jira', + name: 'Jira', + logo: 'test-file-stub', + enabled: true, enabledInConfig: true, enabledInLicense: true, minimumLicenseRequired: 'platinum', diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx index a970fe895eb71..66eef9e3ec7bf 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -32,6 +32,8 @@ import { ActionConnectorTableItem } from '../../../../../../triggers_actions_ui/ import { getCaseUrl } from '../../../../components/link_to'; import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; import { CCMapsCombinedActionAttributes } from '../../../../containers/case/configure/types'; +import { connectorsConfiguration } from '../../../../lib/connectors/config'; + import { Connectors } from '../configure_cases/connectors'; import { ClosureOptions } from '../configure_cases/closure_options'; import { Mapping } from '../configure_cases/mapping'; @@ -54,16 +56,7 @@ const FormWrapper = styled.div` `} `; -const actionTypes: ActionType[] = [ - { - id: '.servicenow', - name: 'ServiceNow', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', - }, -]; +const actionTypes: ActionType[] = Object.values(connectorsConfiguration); interface ConfigureCasesComponentProps { userCanCrud: boolean; diff --git a/x-pack/plugins/siem/public/plugin.tsx b/x-pack/plugins/siem/public/plugin.tsx index f9c44bd341fac..2f2bd70569dcd 100644 --- a/x-pack/plugins/siem/public/plugin.tsx +++ b/x-pack/plugins/siem/public/plugin.tsx @@ -31,7 +31,7 @@ import { SecurityPluginSetup } from '../../security/public'; import { APP_ID, APP_NAME, APP_PATH, APP_ICON } from '../common/constants'; import { initTelemetry } from './lib/telemetry'; import { KibanaServices } from './lib/kibana/services'; -import { serviceNowActionType } from './lib/connectors'; +import { serviceNowActionType, jiraActionType } from './lib/connectors'; export interface SetupPlugins { home: HomePublicPluginSetup; @@ -84,6 +84,7 @@ export class Plugin implements IPlugin getExternalServiceSimulatorPath(service) ); + allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`); + allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/issue`); return allPaths; } @@ -78,9 +82,10 @@ export default function(kibana: any) { }); initPagerduty(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY)); - initServiceNow(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW)); initSlack(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK)); initWebhook(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK)); + initServiceNow(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW)); + initJira(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA)); }, }); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/jira_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/jira_simulation.ts new file mode 100644 index 0000000000000..629d0197b2292 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/jira_simulation.ts @@ -0,0 +1,101 @@ +/* + * 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 Hapi from 'hapi'; + +interface JiraRequest extends Hapi.Request { + payload: { + summary: string; + description?: string; + comments?: string; + }; +} +export function initPlugin(server: Hapi.Server, path: string) { + server.route({ + method: 'POST', + path: `${path}/rest/api/2/issue`, + options: { + auth: false, + }, + handler: createHandler as Hapi.Lifecycle.Method, + }); + + server.route({ + method: 'PUT', + path: `${path}/rest/api/2/issue/{id}`, + options: { + auth: false, + }, + handler: updateHandler as Hapi.Lifecycle.Method, + }); + + server.route({ + method: 'GET', + path: `${path}/rest/api/2/issue/{id}`, + options: { + auth: false, + }, + handler: getHandler as Hapi.Lifecycle.Method, + }); + + server.route({ + method: 'POST', + path: `${path}/rest/api/2/issue/{id}/comment`, + options: { + auth: false, + }, + handler: createCommentHanlder as Hapi.Lifecycle.Method, + }); +} + +// ServiceNow simulator: create a servicenow action pointing here, and you can get +// different responses based on the message posted. See the README.md for +// more info. +function createHandler(request: JiraRequest, h: any) { + return jsonResponse(h, 200, { + id: '123', + key: 'CK-1', + created: '2020-04-27T14:17:45.490Z', + }); +} + +function updateHandler(request: JiraRequest, h: any) { + return jsonResponse(h, 200, { + id: '123', + key: 'CK-1', + created: '2020-04-27T14:17:45.490Z', + updated: '2020-04-27T14:17:45.490Z', + }); +} + +function getHandler(request: JiraRequest, h: any) { + return jsonResponse(h, 200, { + id: '123', + key: 'CK-1', + created: '2020-04-27T14:17:45.490Z', + updated: '2020-04-27T14:17:45.490Z', + summary: 'title', + description: 'description', + }); +} + +function createCommentHanlder(request: JiraRequest, h: any) { + return jsonResponse(h, 200, { + id: '123', + created: '2020-04-27T14:17:45.490Z', + }); +} + +function jsonResponse(h: any, code: number, object?: any) { + if (object == null) { + return h.response('').code(code); + } + + return h + .response(JSON.stringify(object)) + .type('application/json') + .code(code); +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts index a58738e387aeb..cc9521369a47d 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; import Hapi from 'hapi'; interface ServiceNowRequest extends Hapi.Request { @@ -29,18 +28,13 @@ export function initPlugin(server: Hapi.Server, path: string) { path: `${path}/api/now/v2/table/incident/{id}`, options: { auth: false, - validate: { - params: Joi.object({ - id: Joi.string(), - }), - }, }, handler: updateHandler as Hapi.Lifecycle.Method, }); server.route({ method: 'GET', - path: `${path}/api/now/v2/table/incident`, + path: `${path}/api/now/v2/table/incident/{id}`, options: { auth: false, }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts new file mode 100644 index 0000000000000..ed63d25d86aca --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -0,0 +1,549 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + getExternalServiceSimulatorPath, + ExternalServiceSimulator, +} from '../../../../common/fixtures/plugins/actions_simulators'; + +const mapping = [ + { + source: 'title', + target: 'summary', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, +]; + +// eslint-disable-next-line import/no-default-export +export default function jiraTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + + const mockJira = { + config: { + apiUrl: 'www.jiraisinkibanaactions.com', + projectKey: 'CK', + casesConfiguration: { mapping }, + }, + secrets: { + apiToken: 'elastic', + email: 'elastic@elastic.co', + }, + params: { + subAction: 'pushToService', + subActionParams: { + caseId: '123', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + externalId: null, + comments: [ + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ], + }, + }, + }; + + let jiraSimulatorURL: string = ''; + + describe('Jira', () => { + before(() => { + jiraSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) + ); + }); + + after(() => esArchiver.unload('empty_kibana')); + + describe('Jira - Action Creation', () => { + it('should return 200 when creating a jira action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + ...mockJira.config, + apiUrl: jiraSimulatorURL, + }, + secrets: mockJira.secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + isPreconfigured: false, + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + }); + + const { body: fetchedAction } = await supertest + .get(`/api/action/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + isPreconfigured: false, + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with no apiUrl', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { projectKey: 'CK' }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with no projectKey', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { apiUrl: jiraSimulatorURL }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [projectKey]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with a non whitelisted apiUrl', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: 'http://jira.mynonexistent.com', + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + secrets: mockJira.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: error configuring connector action: target url "http://jira.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action without secrets', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [email]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action without casesConfiguration', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + }, + secrets: mockJira.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with empty mapping', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: { mapping: [] }, + }, + secrets: mockJira.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with wrong actionType', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'description', + actionType: 'non-supported', + }, + ], + }, + }, + secrets: mockJira.secrets, + }) + .expect(400); + }); + }); + + describe('Jira - Executor', () => { + let simulatedActionId: string; + before(async () => { + const { body } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira simulator', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + secrets: mockJira.secrets, + }); + simulatedActionId = body.id; + }); + + describe('Validation', () => { + it('should handle failing with a simulated success without action', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: `error validating action params: Cannot read property 'Symbol(Symbol.iterator)' of undefined`, + }); + }); + }); + + it('should handle failing with a simulated success without unsupported action', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'non-supported' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]', + }); + }); + }); + + it('should handle failing with a simulated success without subActionParams', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without caseId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService', subActionParams: {} }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without title', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + caseId: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + caseId: 'success', + title: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.createdAt]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + ...mockJira.params.subActionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{}], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + ...mockJira.params.subActionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + + it('should handle failing with a simulated success without comment.createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + ...mockJira.params.subActionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success', comment: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.createdAt]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + }); + + describe('Execution', () => { + it('should handle creating an incident without comments', async () => { + const { body } = await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + ...mockJira.params.subActionParams, + comments: [], + }, + }, + }) + .expect(200); + + expect(body).to.eql({ + status: 'ok', + actionId: simulatedActionId, + data: { + id: '123', + title: 'CK-1', + pushedDate: '2020-04-27T14:17:45.490Z', + url: `${jiraSimulatorURL}/browse/CK-1`, + }, + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 399ae0f27f5b1..04cd06999f432 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -13,8 +13,6 @@ import { ExternalServiceSimulator, } from '../../../../common/fixtures/plugins/actions_simulators'; -// node ../scripts/functional_test_runner.js --grep "servicenow" --config=test/alerting_api_integration/security_and_spaces/config.ts - const mapping = [ { source: 'title', @@ -24,7 +22,7 @@ const mapping = [ { source: 'description', target: 'description', - actionType: 'append', + actionType: 'overwrite', }, { source: 'comments', @@ -42,40 +40,41 @@ export default function servicenowTest({ getService }: FtrProviderContext) { const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', - casesConfiguration: { mapping: [...mapping] }, + casesConfiguration: { mapping }, }, secrets: { password: 'elastic', username: 'changeme', }, params: { - caseId: '123', - title: 'a title', - description: 'a description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - incidentId: null, - comments: [ - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ], + subAction: 'pushToService', + subActionParams: { + caseId: '123', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + externalId: null, + comments: [ + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ], + }, }, }; - describe('servicenow', () => { - let simulatedActionId = ''; - let servicenowSimulatorURL: string = ''; + let servicenowSimulatorURL: string = ''; - // need to wait for kibanaServer to settle ... + describe('ServiceNow', () => { before(() => { servicenowSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) @@ -84,351 +83,438 @@ export default function servicenowTest({ getService }: FtrProviderContext) { after(() => esArchiver.unload('empty_kibana')); - it('should return 200 when creating a servicenow action successfully', async () => { - const { body: createdAction } = await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ + describe('ServiceNow - Action Creation', () => { + it('should return 200 when creating a servicenow action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: mockServiceNow.config.casesConfiguration, + }, + secrets: mockServiceNow.secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + isPreconfigured: false, name: 'A servicenow action', actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, + casesConfiguration: mockServiceNow.config.casesConfiguration, }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(200); - - expect(createdAction).to.eql({ - id: createdAction.id, - isPreconfigured: false, - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, - }, - }); - - expect(typeof createdAction.id).to.be('string'); - - const { body: fetchedAction } = await supertest - .get(`/api/action/${createdAction.id}`) - .expect(200); - - expect(fetchedAction).to.eql({ - id: fetchedAction.id, - isPreconfigured: false, - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, - }, - }); - }); - - it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: {}, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', - }); }); - }); - it('should respond with a 400 Bad Request when creating a servicenow action with a non whitelisted apiUrl', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: 'http://servicenow.mynonexistent.com', - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: error configuring servicenow action: target url "http://servicenow.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', - }); - }); - }); + const { body: fetchedAction } = await supertest + .get(`/api/action/${createdAction.id}`) + .expect(200); - it('should respond with a 400 Bad Request when creating a servicenow action without secrets', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + isPreconfigured: false, name: 'A servicenow action', actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, + casesConfiguration: mockServiceNow.config.casesConfiguration, }, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type secrets: [password]: expected value of type [string] but got [undefined]', - }); }); - }); + }); - it('should respond with a 400 Bad Request when creating a servicenow action without casesConfiguration', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', + it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', + }); }); - }); - }); + }); - it('should respond with a 400 Bad Request when creating a servicenow action with empty mapping', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { mapping: [] }, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', + it('should respond with a 400 Bad Request when creating a servicenow action with a non whitelisted apiUrl', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: 'http://servicenow.mynonexistent.com', + casesConfiguration: mockServiceNow.config.casesConfiguration, + }, + secrets: mockServiceNow.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: error configuring connector action: target url "http://servicenow.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + }); }); - }); - }); + }); - it('should respond with a 400 Bad Request when creating a servicenow action with wrong actionType', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'description', - actionType: 'non-supported', - }, - ], + it('should respond with a 400 Bad Request when creating a servicenow action without secrets', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: mockServiceNow.config.casesConfiguration, }, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(400); - }); - - it('should create our servicenow simulator action successfully', async () => { - const { body: createdSimulatedAction } = await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow simulator', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(200); + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [password]: expected value of type [string] but got [undefined]', + }); + }); + }); - simulatedActionId = createdSimulatedAction.id; - }); + it('should respond with a 400 Bad Request when creating a servicenow action without casesConfiguration', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + }, + secrets: mockServiceNow.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', + }); + }); + }); - it('should handle executing with a simulated success', async () => { - const { body: result } = await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { ...mockServiceNow.params, title: 'success', comments: [] }, - }) - .expect(200); + it('should respond with a 400 Bad Request when creating a servicenow action with empty mapping', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: { mapping: [] }, + }, + secrets: mockServiceNow.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', + }); + }); + }); - expect(result).to.eql({ - status: 'ok', - actionId: simulatedActionId, - data: { - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, - }, + it('should respond with a 400 Bad Request when creating a servicenow action with wrong actionType', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'description', + actionType: 'non-supported', + }, + ], + }, + }, + secrets: mockServiceNow.secrets, + }) + .expect(400); }); }); - it('should handle failing with a simulated success without caseId', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: {}, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [caseId]: expected value of type [string] but got [undefined]', + describe('ServiceNow - Executor', () => { + let simulatedActionId: string; + before(async () => { + const { body } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow simulator', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: mockServiceNow.config.casesConfiguration, + }, + secrets: mockServiceNow.secrets, }); + simulatedActionId = body.id; + }); + + describe('Validation', () => { + it('should handle failing with a simulated success without action', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: `error validating action params: Cannot read property 'Symbol(Symbol.iterator)' of undefined`, + }); + }); }); - }); - it('should handle failing with a simulated success without title', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { caseId: 'success' }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [title]: expected value of type [string] but got [undefined]', - }); + it('should handle failing with a simulated success without unsupported action', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'non-supported' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]', + }); + }); }); - }); - it('should handle failing with a simulated success without createdAt', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { caseId: 'success', title: 'success' }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [createdAt]: expected value of type [string] but got [undefined]', - }); + it('should handle failing with a simulated success without subActionParams', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + }); + }); }); - }); - it('should handle failing with a simulated success without commentId', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{}], - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [comments.0.commentId]: expected value of type [string] but got [undefined]', - }); + it('should handle failing with a simulated success without caseId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService', subActionParams: {} }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + }); + }); }); - }); - it('should handle failing with a simulated success without comment message', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success' }], - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [comments.0.comment]: expected value of type [string] but got [undefined]', - }); + it('should handle failing with a simulated success without title', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + caseId: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]', + }); + }); }); - }); - it('should handle failing with a simulated success without comment.createdAt', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success', comment: 'success' }], - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ + it('should handle failing with a simulated success without createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + caseId: 'success', + title: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.createdAt]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + ...mockServiceNow.params.subActionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{}], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + ...mockServiceNow.params.subActionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + + it('should handle failing with a simulated success without comment.createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + ...mockServiceNow.params.subActionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success', comment: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.createdAt]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + }); + + describe('Execution', () => { + it('should handle creating an incident without comments', async () => { + const { body: result } = await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + ...mockServiceNow.params.subActionParams, + comments: [], + }, + }, + }) + .expect(200); + + expect(result).to.eql({ + status: 'ok', actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [comments.0.createdAt]: expected value of type [string] but got [undefined]', + data: { + id: '123', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, + }, }); }); + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index 8e002bcc8d3da..18b1714582d13 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -15,6 +15,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./builtin_action_types/pagerduty')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); + loadTestFile(require.resolve('./builtin_action_types/jira')); loadTestFile(require.resolve('./builtin_action_types/slack')); loadTestFile(require.resolve('./builtin_action_types/webhook')); loadTestFile(require.resolve('./create')); From c4e6789c28dd0c8fdd00810537b2687a6e94b760 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 30 Apr 2020 23:44:16 +0200 Subject: [PATCH 054/122] [Lens] Trigger a filter action on click in datatable visualization (#63840) * wip: datatable * fix: empty values * fix: empty values * translations * using dataPlugin to get buckets * one more time, passing aggs data * tests: added * feat: new design applied * remove icon * feat: old design * CR corrections * better name * Fix merge issue * fix: design changes * feat: correction * fix: copy changes * Update x-pack/plugins/lens/public/datatable_visualization/_visualization.scss Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> * Update _visualization.scss Co-authored-by: Elastic Machine Co-authored-by: Wylie Conlon Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> --- .../__snapshots__/expression.test.tsx.snap | 41 +++++ .../_visualization.scss | 10 ++ .../expression.test.tsx | 156 ++++++++++++++++++ .../datatable_visualization/expression.tsx | 146 +++++++++++++--- .../public/datatable_visualization/index.ts | 23 ++- x-pack/plugins/lens/public/plugin.tsx | 1 + .../test/functional/apps/lens/smokescreen.ts | 25 ++- 7 files changed, 373 insertions(+), 29 deletions(-) create mode 100644 x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap create mode 100644 x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx diff --git a/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap new file mode 100644 index 0000000000000..76063d230bdb6 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`datatable_expression DatatableComponent it renders the title and value 1`] = ` + + + +`; diff --git a/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss b/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss index e36326d710f72..7d95d73143870 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss +++ b/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss @@ -1,3 +1,13 @@ .lnsDataTable { align-self: flex-start; } + +.lnsDataTable__filter { + opacity: 0; + transition: opacity $euiAnimSpeedNormal ease-in-out; +} + +.lnsDataTable__cell:hover .lnsDataTable__filter, +.lnsDataTable__filter:focus-within { + opacity: 1; +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx new file mode 100644 index 0000000000000..6d5b1153ad1bc --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { datatable, DatatableComponent } from './expression'; +import { LensMultiTable } from '../types'; +import { DatatableProps } from './expression'; +import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; +import { IFieldFormat } from '../../../../../src/plugins/data/public'; +import { IAggType } from 'src/plugins/data/public'; +const executeTriggerActions = jest.fn(); + +function sampleArgs() { + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + l1: { + type: 'kibana_datatable', + columns: [ + { id: 'a', name: 'a', meta: { type: 'count' } }, + { id: 'b', name: 'b', meta: { type: 'date_histogram', aggConfigParams: { field: 'b' } } }, + { id: 'c', name: 'c', meta: { type: 'cardinality' } }, + ], + rows: [{ a: 10110, b: 1588024800000, c: 3 }], + }, + }, + }; + + const args: DatatableProps['args'] = { + title: 'My fanci metric chart', + columns: { + columnIds: ['a', 'b', 'c'], + type: 'lens_datatable_columns', + }, + }; + + return { data, args }; +} + +describe('datatable_expression', () => { + describe('datatable renders', () => { + test('it renders with the specified data and args', () => { + const { data, args } = sampleArgs(); + const result = datatable.fn(data, args, createMockExecutionContext()); + + expect(result).toEqual({ + type: 'render', + as: 'lens_datatable_renderer', + value: { data, args }, + }); + }); + }); + + describe('DatatableComponent', () => { + test('it renders the title and value', () => { + const { data, args } = sampleArgs(); + + expect( + shallow( + x as IFieldFormat} + executeTriggerActions={executeTriggerActions} + getType={jest.fn()} + /> + ) + ).toMatchSnapshot(); + }); + + test('it invokes executeTriggerActions with correct context on click on top value', () => { + const { args, data } = sampleArgs(); + + const wrapper = mountWithIntl( + x as IFieldFormat} + executeTriggerActions={executeTriggerActions} + getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} + /> + ); + + wrapper + .find('[data-test-subj="lensDatatableFilterOut"]') + .first() + .simulate('click'); + + expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', { + data: { + data: [ + { + column: 0, + row: 0, + table: data.tables.l1, + value: 10110, + }, + ], + negate: true, + }, + timeFieldName: undefined, + }); + }); + + test('it invokes executeTriggerActions with correct context on click on timefield', () => { + const { args, data } = sampleArgs(); + + const wrapper = mountWithIntl( + x as IFieldFormat} + executeTriggerActions={executeTriggerActions} + getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} + /> + ); + + wrapper + .find('[data-test-subj="lensDatatableFilterFor"]') + .at(3) + .simulate('click'); + + expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', { + data: { + data: [ + { + column: 1, + row: 0, + table: data.tables.l1, + value: 1588024800000, + }, + ], + negate: false, + }, + timeFieldName: 'b', + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 772ee13168d02..71d29be1744bb 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -7,7 +7,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n/react'; +import { EuiBasicTable, EuiFlexGroup, EuiButtonIcon, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { IAggType } from 'src/plugins/data/public'; import { FormatFactory, LensMultiTable } from '../types'; import { ExpressionFunctionDefinition, @@ -15,7 +17,10 @@ import { IInterpreterRenderHandlers, } from '../../../../../src/plugins/expressions/public'; import { VisualizationContainer } from '../visualization_container'; - +import { ValueClickTriggerContext } from '../../../../../src/plugins/embeddable/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { getExecuteTriggerActions } from '../services'; export interface DatatableColumns { columnIds: string[]; } @@ -30,6 +35,12 @@ export interface DatatableProps { args: Args; } +type DatatableRenderProps = DatatableProps & { + formatFactory: FormatFactory; + executeTriggerActions: UiActionsStart['executeTriggerActions']; + getType: (name: string) => IAggType; +}; + export interface DatatableRender { type: 'render'; as: 'lens_datatable_renderer'; @@ -100,9 +111,10 @@ export const datatableColumns: ExpressionFunctionDefinition< }, }; -export const getDatatableRenderer = ( - formatFactory: Promise -): ExpressionRenderDefinition => ({ +export const getDatatableRenderer = (dependencies: { + formatFactory: Promise; + getType: Promise<(name: string) => IAggType>; +}): ExpressionRenderDefinition => ({ name: 'lens_datatable_renderer', displayName: i18n.translate('xpack.lens.datatable.visualizationName', { defaultMessage: 'Datatable', @@ -115,9 +127,18 @@ export const getDatatableRenderer = ( config: DatatableProps, handlers: IInterpreterRenderHandlers ) => { - const resolvedFormatFactory = await formatFactory; + const resolvedFormatFactory = await dependencies.formatFactory; + const executeTriggerActions = getExecuteTriggerActions(); + const resolvedGetType = await dependencies.getType; ReactDOM.render( - , + + + , domNode, () => { handlers.done(); @@ -127,7 +148,7 @@ export const getDatatableRenderer = ( }, }); -function DatatableComponent(props: DatatableProps & { formatFactory: FormatFactory }) { +export function DatatableComponent(props: DatatableRenderProps) { const [firstTable] = Object.values(props.data.tables); const formatters: Record> = {}; @@ -135,6 +156,29 @@ function DatatableComponent(props: DatatableProps & { formatFactory: FormatFacto formatters[column.id] = props.formatFactory(column.formatHint); }); + const handleFilterClick = (field: string, value: unknown, colIndex: number, negate = false) => { + const col = firstTable.columns[colIndex]; + const isDateHistogram = col.meta?.type === 'date_histogram'; + const timeFieldName = negate && isDateHistogram ? undefined : col?.meta?.aggConfigParams?.field; + const rowIndex = firstTable.rows.findIndex(row => row[field] === value); + + const context: ValueClickTriggerContext = { + data: { + negate, + data: [ + { + row: rowIndex, + column: colIndex, + value, + table: firstTable, + }, + ], + }, + timeFieldName, + }; + props.executeTriggerActions(VIS_EVENT_TO_TRIGGER.filter, context); + }; + return ( { const col = firstTable.columns.find(c => c.id === field); + const colIndex = firstTable.columns.findIndex(c => c.id === field); + + const filterable = col?.meta?.type && props.getType(col.meta.type)?.type === 'buckets'; return { field, name: (col && col.name) || '', + render: (value: unknown) => { + const formattedValue = formatters[field]?.convert(value); + const fieldName = col?.meta?.aggConfigParams?.field; + + if (filterable) { + return ( + + {formattedValue} + + + + handleFilterClick(field, value, colIndex)} + /> + + + + handleFilterClick(field, value, colIndex, true)} + /> + + + + + + ); + } + return {formattedValue}; + }, }; }) .filter(({ field }) => !!field)} - items={ - firstTable - ? firstTable.rows.map(row => { - const formattedRow: Record = {}; - Object.entries(formatters).forEach(([columnId, formatter]) => { - formattedRow[columnId] = formatter.convert(row[columnId]); - }); - return formattedRow; - }) - : [] - } + items={firstTable ? firstTable.rows : []} /> ); diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts index ff036aadfd4cf..44894d31da51d 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/index.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts @@ -4,12 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'kibana/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; import { datatableVisualization } from './visualization'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { datatable, datatableColumns, getDatatableRenderer } from './expression'; import { EditorFrameSetup, FormatFactory } from '../types'; +import { setExecuteTriggerActions } from '../services'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +interface DatatableVisualizationPluginStartPlugins { + uiActions: UiActionsStart; + data: DataPublicPluginStart; +} export interface DatatableVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; formatFactory: Promise; @@ -20,12 +27,22 @@ export class DatatableVisualization { constructor() {} setup( - _core: CoreSetup | null, + core: CoreSetup, { expressions, formatFactory, editorFrame }: DatatableVisualizationPluginSetupPlugins ) { expressions.registerFunction(() => datatableColumns); expressions.registerFunction(() => datatable); - expressions.registerRenderer(() => getDatatableRenderer(formatFactory)); + expressions.registerRenderer(() => + getDatatableRenderer({ + formatFactory, + getType: core + .getStartServices() + .then(([_, { data: dataStart }]) => dataStart.search.aggs.types.get), + }) + ); editorFrame.registerVisualization(datatableVisualization); } + start(core: CoreStart, { uiActions }: DatatableVisualizationPluginStartPlugins) { + setExecuteTriggerActions(uiActions.executeTriggerActions); + } } diff --git a/x-pack/plugins/lens/public/plugin.tsx b/x-pack/plugins/lens/public/plugin.tsx index 8d760eb0df501..fe0e81177e259 100644 --- a/x-pack/plugins/lens/public/plugin.tsx +++ b/x-pack/plugins/lens/public/plugin.tsx @@ -200,6 +200,7 @@ export class LensPlugin { start(core: CoreStart, startDependencies: LensPluginStartDependencies) { this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance; this.xyVisualization.start(core, startDependencies); + this.datatableVisualization.start(core, startDependencies); } stop() { diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index be7a2faae6711..082008bccddd1 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -26,12 +26,12 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); - async function assertExpectedMetric() { + async function assertExpectedMetric(metricCount: string = '19,986') { await PageObjects.lens.assertExactText( '[data-test-subj="lns_metric_title"]', 'Maximum of bytes' ); - await PageObjects.lens.assertExactText('[data-test-subj="lns_metric_value"]', '19,986'); + await PageObjects.lens.assertExactText('[data-test-subj="lns_metric_value"]', metricCount); } async function assertExpectedTable() { @@ -40,8 +40,12 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { 'Maximum of bytes' ); await PageObjects.lens.assertExactText( - '[data-test-subj="lnsDataTable"] tbody .euiTableCellContent__text', - '19,986' + '[data-test-subj="lnsDataTable"] [data-test-subj="lnsDataTableCellValue"]', + '19,985' + ); + await PageObjects.lens.assertExactText( + '[data-test-subj="lnsDataTable"] [data-test-subj="lnsDataTableCellValueFilterable"]', + 'IN' ); } @@ -86,7 +90,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await assertExpectedMetric(); }); - it('click on the bar in XYChart adds proper filters/timerange', async () => { + it('click on the bar in XYChart adds proper filters/timerange in dashboard', async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await dashboardAddPanel.clickOpenAddPanel(); @@ -102,15 +106,22 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { expect(hasIpFilter).to.be(true); }); - it('should allow seamless transition to and from table view', async () => { + it('should allow seamless transition to and from table view and add a filter', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); await PageObjects.lens.goToTimeRange(); await assertExpectedMetric(); await PageObjects.lens.switchToVisualization('lnsChartSwitchPopover_lnsDatatable'); + await PageObjects.lens.configureDimension({ + dimension: '[data-test-subj="lnsDatatable_column"] [data-test-subj="lns-empty-dimension"]', + operation: 'terms', + field: 'geo.dest', + }); + await PageObjects.lens.save('Artistpreviouslyknownaslens'); + await find.clickByCssSelector('[data-test-subj="lensDatatableFilterOut"]'); await assertExpectedTable(); await PageObjects.lens.switchToVisualization('lnsChartSwitchPopover_lnsMetric'); - await assertExpectedMetric(); + await assertExpectedMetric('19,985'); }); it('should allow creation of lens visualizations', async () => { From 127b324a5f400b9e26357529930879e24d5e9770 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Thu, 30 Apr 2020 14:45:50 -0700 Subject: [PATCH 055/122] [Canvas] Adds refresh and autoplay options to view menu (#64375) --- .../legacy/plugins/canvas/i18n/components.ts | 22 +++- .../control_settings/control_settings.scss | 7 - .../control_settings/control_settings.tsx | 84 ------------ .../workpad_header/control_settings/index.ts | 35 ----- .../refresh_control/refresh_control.tsx | 1 + .../__snapshots__/view_menu.stories.storyshot | 66 ++++++++++ .../__examples__/view_menu.stories.tsx | 58 ++++++--- .../auto_refresh_controls.tsx | 36 +---- .../custom_interval.tsx | 0 .../workpad_header/view_menu/index.ts | 39 ++++-- .../kiosk_controls.tsx | 26 +--- .../workpad_header/view_menu/view_menu.scss | 4 + .../workpad_header/view_menu/view_menu.tsx | 123 ++++++++++++++++-- .../workpad_header/workpad_header.tsx | 6 +- .../plugins/canvas/public/style/index.scss | 2 +- .../footer/settings/autoplay_settings.tsx | 2 +- .../functional/page_objects/canvas_page.ts | 10 +- 17 files changed, 289 insertions(+), 232 deletions(-) delete mode 100644 x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss delete mode 100644 x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.tsx delete mode 100644 x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/index.ts rename x-pack/legacy/plugins/canvas/public/components/workpad_header/{control_settings => view_menu}/auto_refresh_controls.tsx (83%) rename x-pack/legacy/plugins/canvas/public/components/workpad_header/{control_settings => view_menu}/custom_interval.tsx (100%) rename x-pack/legacy/plugins/canvas/public/components/workpad_header/{control_settings => view_menu}/kiosk_controls.tsx (86%) create mode 100644 x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.scss diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts index 7bd16c4933ce1..b1c2b41f062f1 100644 --- a/x-pack/legacy/plugins/canvas/i18n/components.ts +++ b/x-pack/legacy/plugins/canvas/i18n/components.ts @@ -1079,12 +1079,6 @@ export const ComponentStrings = { defaultMessage: 'Refresh elements', }), }, - WorkpadHeaderControlSettings: { - getButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderControlSettings.buttonLabel', { - defaultMessage: 'Options', - }), - }, WorkpadHeaderCustomInterval: { getButtonLabel: () => i18n.translate('xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel', { @@ -1305,6 +1299,18 @@ export const ComponentStrings = { }), }, WorkpadHeaderViewMenu: { + getAutoplayOffMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplayOffMenuItemLabel', { + defaultMessage: 'Turn autoplay off', + }), + getAutoplayOnMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplayOnMenuItemLabel', { + defaultMessage: 'Turn autoplay on', + }), + getAutoplaySettingsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplaySettingsMenuItemLabel', { + defaultMessage: 'Autoplay settings', + }), getFullscreenMenuItemLabel: () => i18n.translate('xpack.canvas.workpadHeaderViewMenu.fullscreenMenuLabel', { defaultMessage: 'Enter fullscreen mode', @@ -1317,6 +1323,10 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.workpadHeaderViewMenu.refreshMenuItemLabel', { defaultMessage: 'Refresh data', }), + getRefreshSettingsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.refreshSettingsMenuItemLabel', { + defaultMessage: 'Auto refresh settings', + }), getShowEditModeLabel: () => i18n.translate('xpack.canvas.workpadHeaderViewMenu.showEditModeLabel', { defaultMessage: 'Show editing controls', diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss deleted file mode 100644 index 3d217dd1fc180..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss +++ /dev/null @@ -1,7 +0,0 @@ -.canvasControlSettings__popover { - width: 600px; -} - -.canvasControlSettings__list { - columns: 2; -} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.tsx deleted file mode 100644 index adc57ff4f815a..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.tsx +++ /dev/null @@ -1,84 +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, { MouseEventHandler } from 'react'; -import PropTypes from 'prop-types'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; -// @ts-ignore untyped local -import { Popover } from '../../popover'; -import { AutoRefreshControls } from './auto_refresh_controls'; -import { KioskControls } from './kiosk_controls'; - -import { ComponentStrings } from '../../../../i18n'; -const { WorkpadHeaderControlSettings: strings } = ComponentStrings; - -interface Props { - refreshInterval: number; - setRefreshInterval: (interval: number | undefined) => void; - autoplayEnabled: boolean; - autoplayInterval: number; - enableAutoplay: (enable: boolean) => void; - setAutoplayInterval: (interval: number | undefined) => void; -} - -export const ControlSettings = ({ - setRefreshInterval, - refreshInterval, - autoplayEnabled, - autoplayInterval, - enableAutoplay, - setAutoplayInterval, -}: Props) => { - const setRefresh = (val: number | undefined) => setRefreshInterval(val); - - const disableInterval = () => { - setRefresh(0); - }; - - const popoverButton = (handleClick: MouseEventHandler) => ( - - {strings.getButtonLabel()} - - ); - - return ( - - {() => ( - - - setRefresh(val)} - disableInterval={() => disableInterval()} - /> - - - - - - )} - - ); -}; - -ControlSettings.propTypes = { - refreshInterval: PropTypes.number, - setRefreshInterval: PropTypes.func.isRequired, - autoplayEnabled: PropTypes.bool, - autoplayInterval: PropTypes.number, - enableAutoplay: PropTypes.func.isRequired, - setAutoplayInterval: PropTypes.func.isRequired, -}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/index.ts deleted file mode 100644 index 316a49c85c09d..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/index.ts +++ /dev/null @@ -1,35 +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 { connect } from 'react-redux'; -import { - setRefreshInterval, - enableAutoplay, - setAutoplayInterval, - // @ts-ignore untyped local -} from '../../../state/actions/workpad'; -// @ts-ignore untyped local -import { getRefreshInterval, getAutoplay } from '../../../state/selectors/workpad'; -import { State } from '../../../../types'; -import { ControlSettings as Component } from './control_settings'; - -const mapStateToProps = (state: State) => { - const { enabled, interval } = getAutoplay(state); - - return { - refreshInterval: getRefreshInterval(state), - autoplayEnabled: enabled, - autoplayInterval: interval, - }; -}; - -const mapDispatchToProps = { - setRefreshInterval, - enableAutoplay, - setAutoplayInterval, -}; - -export const ControlSettings = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx index 1768adf9be79d..d651e649128f9 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx @@ -32,6 +32,7 @@ export const RefreshControl = ({ doRefresh, inFlight }: Props) => ( iconType="refresh" aria-label={strings.getRefreshAriaLabel()} onClick={doRefresh} + data-test-subj="canvas-refresh-control" /> ); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot index e1ecee0e152be..eb45f97452ae1 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot @@ -65,3 +65,69 @@ exports[`Storyshots components/WorkpadHeader/ViewMenu read only mode 1`] = `
`; + +exports[`Storyshots components/WorkpadHeader/ViewMenu with autoplay enabled 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots components/WorkpadHeader/ViewMenu with refresh enabled 1`] = ` +
+
+ +
+
+`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx index 60837ac1218e6..5b4de05da3a3d 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx @@ -8,32 +8,58 @@ import { action } from '@storybook/addon-actions'; import React from 'react'; import { ViewMenu } from '../view_menu'; +const handlers = { + setZoomScale: action('setZoomScale'), + zoomIn: action('zoomIn'), + zoomOut: action('zoomOut'), + toggleWriteable: action('toggleWriteable'), + resetZoom: action('resetZoom'), + enterFullscreen: action('enterFullscreen'), + doRefresh: action('doRefresh'), + fitToWindow: action('fitToWindow'), + setRefreshInterval: action('setRefreshInterval'), + setAutoplayInterval: action('setAutoplayInterval'), + enableAutoplay: action('enableAutoplay'), +}; + storiesOf('components/WorkpadHeader/ViewMenu', module) .add('edit mode', () => ( )) .add('read only mode', () => ( + )) + .add('with refresh enabled', () => ( + + )) + .add('with autoplay enabled', () => ( + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx similarity index 83% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx index 97d8920d50dd3..cfd599b1d9f3f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx @@ -22,7 +22,6 @@ import { htmlIdGenerator, } from '@elastic/eui'; import { timeDuration } from '../../../lib/time_duration'; -import { RefreshControl } from '../refresh_control'; import { CustomInterval } from './custom_interval'; import { ComponentStrings, UnitStrings } from '../../../../i18n'; @@ -69,7 +68,11 @@ export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterv const intervalTitleId = generateId(); return ( - + @@ -97,9 +100,6 @@ export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterv ) : null} - - - @@ -112,16 +112,6 @@ export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterv - - - - - - setRefresh(value)} /> diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx similarity index 100% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts index eee613183639c..e1ad9782c8aef 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts @@ -15,13 +15,21 @@ import { fetchAllRenderables } from '../../../state/actions/elements'; // @ts-ignore Untyped local import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; // @ts-ignore Untyped local -import { setWriteable } from '../../../state/actions/workpad'; +import { + setWriteable, + setRefreshInterval, + enableAutoplay, + setAutoplayInterval, + // @ts-ignore Untyped local +} from '../../../state/actions/workpad'; import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; import { getWorkpadBoundingBox, getWorkpadWidth, getWorkpadHeight, isWriteable, + getRefreshInterval, + getAutoplay, } from '../../../state/selectors/workpad'; import { ViewMenu as Component, Props as ComponentProps } from './view_menu'; import { getFitZoomScale } from './lib/get_fit_zoom_scale'; @@ -40,24 +48,35 @@ interface DispatchProps { setFullscreen: (showFullscreen: boolean) => void; } -const mapStateToProps = (state: State) => ({ - zoomScale: getZoomScale(state), - boundingBox: getWorkpadBoundingBox(state), - workpadWidth: getWorkpadWidth(state), - workpadHeight: getWorkpadHeight(state), - isWriteable: isWriteable(state) && canUserWrite(state), -}); +const mapStateToProps = (state: State) => { + const { enabled, interval } = getAutoplay(state); + + return { + zoomScale: getZoomScale(state), + boundingBox: getWorkpadBoundingBox(state), + workpadWidth: getWorkpadWidth(state), + workpadHeight: getWorkpadHeight(state), + isWriteable: isWriteable(state) && canUserWrite(state), + refreshInterval: getRefreshInterval(state), + autoplayEnabled: enabled, + autoplayInterval: interval, + }; +}; const mapDispatchToProps = (dispatch: Dispatch) => ({ setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), setFullscreen: (value: boolean) => { dispatch(setFullscreen(value)); + if (value) { dispatch(selectToplevelNodes([])); } }, doRefresh: () => dispatch(fetchAllRenderables()), + setRefreshInterval: (interval: number) => dispatch(setRefreshInterval(interval)), + enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(autoplay)), + setAutoplayInterval: (interval: number) => dispatch(setAutoplayInterval(interval)), }); const mergeProps = ( @@ -66,13 +85,15 @@ const mergeProps = ( ownProps: ComponentProps ): ComponentProps => { const { boundingBox, workpadWidth, workpadHeight, ...remainingStateProps } = stateProps; + return { ...remainingStateProps, ...dispatchProps, ...ownProps, toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), enterFullscreen: () => dispatchProps.setFullscreen(true), - fitToWindow: () => getFitZoomScale(boundingBox, workpadWidth, workpadHeight), + fitToWindow: () => + dispatchProps.setZoomScale(getFitZoomScale(boundingBox, workpadWidth, workpadHeight)), }; }; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx similarity index 86% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx index 9e6f0a91c6120..e63eed9f9df53 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx @@ -14,7 +14,6 @@ import { EuiHorizontalRule, EuiLink, EuiSpacer, - EuiSwitch, EuiText, EuiFlexItem, EuiFlexGroup, @@ -29,9 +28,7 @@ const { time: timeStrings } = UnitStrings; const { getSecondsText, getMinutesText } = timeStrings; interface Props { - autoplayEnabled: boolean; autoplayInterval: number; - onSetEnabled: (enabled: boolean) => void; onSetInterval: (interval: number | undefined) => void; } @@ -54,12 +51,7 @@ const ListGroup = ({ children, ...rest }: ListGroupProps) => ( const generateId = htmlIdGenerator(); -export const KioskControls = ({ - autoplayEnabled, - autoplayInterval, - onSetEnabled, - onSetInterval, -}: Props) => { +export const KioskControls = ({ autoplayInterval, onSetInterval }: Props) => { const RefreshItem = ({ duration, label, descriptionId }: RefreshItemProps) => (
  • onSetInterval(duration)} aria-describedby={descriptionId}> @@ -72,7 +64,11 @@ export const KioskControls = ({ const intervalTitleId = generateId(); return ( - + {strings.getTitle()} @@ -81,14 +77,6 @@ export const KioskControls = ({ - - onSetEnabled(ev.target.checked)} - /> - -

    {strings.getCycleFormLabel()}

    @@ -137,8 +125,6 @@ export const KioskControls = ({ }; KioskControls.propTypes = { - autoplayEnabled: PropTypes.bool.isRequired, autoplayInterval: PropTypes.number.isRequired, - onSetEnabled: PropTypes.func.isRequired, onSetInterval: PropTypes.func.isRequired, }; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.scss new file mode 100644 index 0000000000000..c4e06881981c7 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.scss @@ -0,0 +1,4 @@ +.canvasViewMenu__kioskSettings, +.canvasViewMenu__refreshSettings { + padding: $euiSize; +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx index d1e08c5809579..b6f108cda37f6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx @@ -12,10 +12,16 @@ import { EuiIcon, EuiContextMenuPanelItemDescriptor, } from '@elastic/eui'; -import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../../../common/lib/constants'; +import { + MAX_ZOOM_LEVEL, + MIN_ZOOM_LEVEL, + CONTEXT_MENU_TOP_BORDER_CLASSNAME, +} from '../../../../common/lib/constants'; import { ComponentStrings } from '../../../../i18n/components'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { Popover, ClosePopoverFn } from '../../popover'; +import { AutoRefreshControls } from './auto_refresh_controls'; +import { KioskControls } from './kiosk_controls'; const { WorkpadHeaderViewMenu: strings } = ComponentStrings; @@ -62,10 +68,33 @@ export interface Props { * triggers a refresh of the workpad */ doRefresh: () => void; + /** + * Current auto refresh interval + */ + refreshInterval: number; + /** + * Sets auto refresh interval + */ + setRefreshInterval: (interval?: number) => void; + /** + * Is autoplay enabled? + */ + autoplayEnabled: boolean; + /** + * Current autoplay interval + */ + autoplayInterval: number; + /** + * Enables autoplay + */ + enableAutoplay: (autoplay: boolean) => void; + /** + * Sets autoplay interval + */ + setAutoplayInterval: (interval?: number) => void; } export const ViewMenu: FunctionComponent = ({ - doRefresh, enterFullscreen, fitToWindow, isWriteable, @@ -75,7 +104,20 @@ export const ViewMenu: FunctionComponent = ({ zoomIn, zoomOut, zoomScale, + doRefresh, + refreshInterval, + setRefreshInterval, + autoplayEnabled, + autoplayInterval, + enableAutoplay, + setAutoplayInterval, }) => { + const setRefresh = (val: number | undefined) => setRefreshInterval(val); + + const disableInterval = () => { + setRefresh(0); + }; + const viewControl = (togglePopover: React.MouseEventHandler) => ( {strings.getViewMenuButtonLabel()} @@ -121,36 +163,76 @@ export const ViewMenu: FunctionComponent = ({ const getPanelTree = (closePopover: ClosePopoverFn) => ({ id: 0, - title: strings.getViewMenuLabel(), items: [ + { + name: strings.getRefreshMenuItemLabel(), + icon: 'refresh', + onClick: () => { + doRefresh(); + }, + }, + { + name: strings.getRefreshSettingsMenuItemLabel(), + icon: 'empty', + panel: { + id: 1, + title: strings.getRefreshSettingsMenuItemLabel(), + content: ( + setRefresh(val)} + disableInterval={() => disableInterval()} + /> + ), + }, + }, { name: strings.getFullscreenMenuItemLabel(), icon: , + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, onClick: () => { enterFullscreen(); closePopover(); }, }, { - name: isWriteable ? strings.getHideEditModeLabel() : strings.getShowEditModeLabel(), - icon: , + name: autoplayEnabled + ? strings.getAutoplayOffMenuItemLabel() + : strings.getAutoplayOnMenuItemLabel(), + icon: autoplayEnabled ? 'stop' : 'play', onClick: () => { - toggleWriteable(); + enableAutoplay(!autoplayEnabled); closePopover(); }, }, { - name: strings.getRefreshMenuItemLabel(), - icon: 'refresh', + name: strings.getAutoplaySettingsMenuItemLabel(), + icon: 'empty', + panel: { + id: 2, + title: strings.getAutoplaySettingsMenuItemLabel(), + content: ( + + ), + }, + }, + { + name: isWriteable ? strings.getHideEditModeLabel() : strings.getShowEditModeLabel(), + icon: , + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, onClick: () => { - doRefresh(); + toggleWriteable(); + closePopover(); }, }, { name: strings.getZoomMenuItemLabel(), icon: 'magnifyWithPlus', panel: { - id: 1, + id: 3, title: strings.getZoomMenuItemLabel(), items: getZoomMenuItems(), }, @@ -161,7 +243,11 @@ export const ViewMenu: FunctionComponent = ({ return ( {({ closePopover }: { closePopover: ClosePopoverFn }) => ( - + )} ); @@ -169,4 +255,19 @@ export const ViewMenu: FunctionComponent = ({ ViewMenu.propTypes = { isWriteable: PropTypes.bool.isRequired, + zoomScale: PropTypes.number.isRequired, + fitToWindow: PropTypes.func.isRequired, + setZoomScale: PropTypes.func.isRequired, + zoomIn: PropTypes.func.isRequired, + zoomOut: PropTypes.func.isRequired, + resetZoom: PropTypes.func.isRequired, + toggleWriteable: PropTypes.func.isRequired, + enterFullscreen: PropTypes.func.isRequired, + doRefresh: PropTypes.func.isRequired, + refreshInterval: PropTypes.number.isRequired, + setRefreshInterval: PropTypes.func.isRequired, + autoplayEnabled: PropTypes.bool.isRequired, + autoplayInterval: PropTypes.number.isRequired, + enableAutoplay: PropTypes.func.isRequired, + setAutoplayInterval: PropTypes.func.isRequired, }; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx index 253e6c68cfc9e..1f8e47b556771 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx @@ -11,7 +11,6 @@ import { Shortcuts } from 'react-shortcuts'; import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { ComponentStrings } from '../../../i18n'; import { ToolTipShortcut } from '../tool_tip_shortcut/'; -import { ControlSettings } from './control_settings'; // @ts-ignore untyped local import { RefreshControl } from './refresh_control'; // @ts-ignore untyped local @@ -103,9 +102,6 @@ export const WorkpadHeader: FunctionComponent = ({ - - -
    @@ -122,7 +118,7 @@ export const WorkpadHeader: FunctionComponent = ({ )} void; export type onSetIntervalFn = (interval: string) => void; diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index 94ad393ead3a3..ce36385a2f9df 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -65,14 +65,12 @@ export function CanvasPageProvider({ getService }: FtrProviderContext) { async expectNoAddElementButton() { // Ensure page is fully loaded first by waiting for the refresh button - const refreshPopoverExists = await find.existsByCssSelector('#auto-refresh-popover', 20000); + const refreshPopoverExists = await testSubjects.exists('canvas-refresh-control', { + timeout: 20000, + }); expect(refreshPopoverExists).to.be(true); - const addElementButtonExists = await find.existsByCssSelector( - 'button[data-test-subj=add-element-button]', - 10 // don't need much of a wait at all here, because we already waited for refresh button above - ); - expect(addElementButtonExists).to.be(false); + await testSubjects.missingOrFail('add-element-button'); }, }; } From f9c1033d41aac8dd86b05ca2ad01cd12b51cca69 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Thu, 30 Apr 2020 14:51:24 -0700 Subject: [PATCH 056/122] [Canvas] Adds edit menu (#64738) --- .../legacy/plugins/canvas/i18n/components.ts | 162 ++--- .../legacy/plugins/canvas/i18n/shortcuts.ts | 4 +- .../apps/workpad/workpad_app/workpad_app.js | 4 +- .../keyboard_shortcuts_doc.stories.storyshot | 4 +- .../components/sidebar/sidebar_content.js | 40 +- .../sidebar_header.stories.storyshot | 140 ---- .../__examples__/sidebar_header.stories.tsx | 16 - .../sidebar_header/sidebar_header.tsx | 627 +++--------------- .../edit_menu.examples.storyshot | 205 ++++++ .../__examples__/edit_menu.examples.tsx | 69 ++ .../workpad_header/edit_menu/edit_menu.tsx | 448 +++++++++++++ .../workpad_header/edit_menu/index.ts | 123 ++++ .../element_menu/element_menu.tsx | 1 - .../workpad_header/share_menu/share_menu.tsx | 1 - .../workpad_header/workpad_header.tsx | 6 + .../canvas/public/state/selectors/workpad.ts | 6 +- .../legacy/plugins/canvas/types/elements.ts | 6 +- .../translations/translations/ja-JP.json | 28 +- .../translations/translations/zh-CN.json | 31 +- .../functional/apps/canvas/custom_elements.ts | 5 +- 20 files changed, 1100 insertions(+), 826 deletions(-) create mode 100644 x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.examples.storyshot create mode 100644 x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx create mode 100644 x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx create mode 100644 x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/index.ts diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts index b1c2b41f062f1..de16bc2101e8c 100644 --- a/x-pack/legacy/plugins/canvas/i18n/components.ts +++ b/x-pack/legacy/plugins/canvas/i18n/components.ts @@ -804,17 +804,6 @@ export const ComponentStrings = { }), }, SidebarHeader: { - getAlignmentMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.alignmentMenuItemLabel', { - defaultMessage: 'Alignment', - description: - 'This refers to the vertical (i.e. left, center, right) and horizontal (i.e. top, middle, bottom) ' + - 'alignment options of the selected elements', - }), - getBottomAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.bottomAlignMenuItemLabel', { - defaultMessage: 'Bottom', - }), getBringForwardAriaLabel: () => i18n.translate('xpack.canvas.sidebarHeader.bringForwardArialLabel', { defaultMessage: 'Move element up one layer', @@ -823,56 +812,6 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.sidebarHeader.bringToFrontArialLabel', { defaultMessage: 'Move element to top layer', }), - getCenterAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.centerAlignMenuItemLabel', { - defaultMessage: 'Center', - description: 'This refers to alignment centered horizontally.', - }), - getContextMenuTitle: () => - i18n.translate('xpack.canvas.sidebarHeader.contextMenuAriaLabel', { - defaultMessage: 'Element options', - }), - getCreateElementModalTitle: () => - i18n.translate('xpack.canvas.sidebarHeader.createElementModalTitle', { - defaultMessage: 'Create new element', - }), - getDistributionMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.distributionMenutItemLabel', { - defaultMessage: 'Distribution', - description: - 'This refers to the options to evenly spacing the selected elements horizontall or vertically.', - }), - getGroupMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.groupMenuItemLabel', { - defaultMessage: 'Group', - description: 'This refers to grouping multiple selected elements.', - }), - getHorizontalDistributionMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.horizontalDistributionMenutItemLabel', { - defaultMessage: 'Horizontal', - }), - getLeftAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.leftAlignMenuItemLabel', { - defaultMessage: 'Left', - }), - getMiddleAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.middleAlignMenuItemLabel', { - defaultMessage: 'Middle', - description: 'This refers to alignment centered vertically.', - }), - getOrderMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.orderMenuItemLabel', { - defaultMessage: 'Order', - description: 'Refers to the order of the elements displayed on the page from front to back', - }), - getRightAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.rightAlignMenuItemLabel', { - defaultMessage: 'Right', - }), - getSaveElementMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.savedElementMenuItemLabel', { - defaultMessage: 'Save as new element', - }), getSendBackwardAriaLabel: () => i18n.translate('xpack.canvas.sidebarHeader.sendBackwardArialLabel', { defaultMessage: 'Move element down one layer', @@ -881,19 +820,6 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.sidebarHeader.sendToBackArialLabel', { defaultMessage: 'Move element to bottom layer', }), - getTopAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.topAlignMenuItemLabel', { - defaultMessage: 'Top', - }), - getUngroupMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.ungroupMenuItemLabel', { - defaultMessage: 'Ungroup', - description: 'This refers to ungrouping a grouped element', - }), - getVerticalDistributionMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel', { - defaultMessage: 'Vertical', - }), }, TextStylePicker: { getAlignCenterOption: () => @@ -1099,6 +1025,94 @@ export const ComponentStrings = { defaultMessage: 'Set a custom interval', }), }, + WorkpadHeaderEditMenu: { + getAlignmentMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel', { + defaultMessage: 'Alignment', + description: + 'This refers to the vertical (i.e. left, center, right) and horizontal (i.e. top, middle, bottom) ' + + 'alignment options of the selected elements', + }), + getBottomAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel', { + defaultMessage: 'Bottom', + }), + getCenterAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel', { + defaultMessage: 'Center', + description: 'This refers to alignment centered horizontally.', + }), + getCreateElementModalTitle: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.createElementModalTitle', { + defaultMessage: 'Create new element', + }), + getDistributionMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel', { + defaultMessage: 'Distribution', + description: + 'This refers to the options to evenly spacing the selected elements horizontall or vertically.', + }), + getEditMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuButtonLabel', { + defaultMessage: 'Edit', + }), + getEditMenuLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuLabel', { + defaultMessage: 'Edit options', + }), + getGroupMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel', { + defaultMessage: 'Group', + description: 'This refers to grouping multiple selected elements.', + }), + getHorizontalDistributionMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel', { + defaultMessage: 'Horizontal', + }), + getLeftAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel', { + defaultMessage: 'Left', + }), + getMiddleAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel', { + defaultMessage: 'Middle', + description: 'This refers to alignment centered vertically.', + }), + getOrderMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel', { + defaultMessage: 'Order', + description: 'Refers to the order of the elements displayed on the page from front to back', + }), + getRedoMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.redoMenuItemLabel', { + defaultMessage: 'Redo', + }), + getRightAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel', { + defaultMessage: 'Right', + }), + getSaveElementMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel', { + defaultMessage: 'Save as new element', + }), + getTopAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.topAlignMenuItemLabel', { + defaultMessage: 'Top', + }), + getUndoMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.undoMenuItemLabel', { + defaultMessage: 'Undo', + }), + getUngroupMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.ungroupMenuItemLabel', { + defaultMessage: 'Ungroup', + description: 'This refers to ungrouping a grouped element', + }), + getVerticalDistributionMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.verticalDistributionMenutItemLabel', { + defaultMessage: 'Vertical', + }), + }, WorkpadHeaderElementMenu: { getAssetsMenuItemLabel: () => i18n.translate('xpack.canvas.workpadHeaderElementMenu.manageAssetsMenuItemLabel', { diff --git a/x-pack/legacy/plugins/canvas/i18n/shortcuts.ts b/x-pack/legacy/plugins/canvas/i18n/shortcuts.ts index 32b07a45c17db..124d70ff3095f 100644 --- a/x-pack/legacy/plugins/canvas/i18n/shortcuts.ts +++ b/x-pack/legacy/plugins/canvas/i18n/shortcuts.ts @@ -42,10 +42,10 @@ export const ShortcutStrings = { defaultMessage: 'Delete', }), BRING_FORWARD: i18n.translate('xpack.canvas.keyboardShortcuts.bringFowardShortcutHelpText', { - defaultMessage: 'Bring to front', + defaultMessage: 'Bring forward', }), BRING_TO_FRONT: i18n.translate('xpack.canvas.keyboardShortcuts.bringToFrontShortcutHelpText', { - defaultMessage: 'Bring forward', + defaultMessage: 'Bring to front', }), SEND_BACKWARD: i18n.translate('xpack.canvas.keyboardShortcuts.sendBackwardShortcutHelpText', { defaultMessage: 'Send backward', diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js index b8e1bece6ac30..fc3ac9922355a 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js @@ -43,7 +43,7 @@ export class WorkpadApp extends React.PureComponent {
    - + {})} />
    - {})} /> +
    )}
    diff --git a/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot index 503677687ba12..d59d03578a363 100644 --- a/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot @@ -212,7 +212,7 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = `
    - Bring forward + Bring to front
    - Bring to front + Bring forward
    ({ selectedToplevelNodes: getSelectedToplevelNodes(state), selectedElementId: getSelectedElementId(state), - state, }); -const mergeProps = ( - { state, ...restStateProps }, - { dispatch, ...restDispatchProps }, - ownProps -) => ({ - ...ownProps, - ...restDispatchProps, - ...restStateProps, - updateGlobalState: globalStateUpdater(dispatch, state), -}); - -const withGlobalState = (commit, updateGlobalState) => (type, payload) => { - const newLayoutState = commit(type, payload); - if (newLayoutState.currentScene.gestureEnd) { - updateGlobalState(newLayoutState); - } -}; - -const MultiElementSidebar = ({ commit, updateGlobalState }) => ( +const MultiElementSidebar = () => ( - + ); -const GroupedElementSidebar = ({ commit, updateGlobalState }) => ( +const GroupedElementSidebar = () => ( - + @@ -92,7 +65,4 @@ const branches = [ ), ]; -export const SidebarContent = compose( - connect(mapStateToProps, null, mergeProps), - ...branches -)(GlobalConfig); +export const SidebarContent = compose(connect(mapStateToProps), ...branches)(GlobalConfig); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot index ac25cbe0b6832..4d5b9570ee20f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot @@ -20,80 +20,6 @@ exports[`Storyshots components/Sidebar/SidebarHeader default 1`] = ` Selected layer
    -
    -
    -
    - - - -
    -
    - -
    -
    -
  • `; @@ -224,72 +150,6 @@ exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` -
    - - - -
    -
    - -
    diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx index 9baff380fc3eb..11c66906a6ef6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx @@ -10,26 +10,10 @@ import { action } from '@storybook/addon-actions'; import { SidebarHeader } from '../sidebar_header'; const handlers = { - cloneNodes: action('cloneNodes'), - copyNodes: action('copyNodes'), - cutNodes: action('cutNodes'), - pasteNodes: action('pasteNodes'), - deleteNodes: action('deleteNodes'), bringToFront: action('bringToFront'), bringForward: action('bringForward'), sendBackward: action('sendBackward'), sendToBack: action('sendToBack'), - createCustomElement: action('createCustomElement'), - groupNodes: action('groupNodes'), - ungroupNodes: action('ungroupNodes'), - alignLeft: action('alignLeft'), - alignMiddle: action('alignMiddle'), - alignRight: action('alignRight'), - alignTop: action('alignTop'), - alignCenter: action('alignCenter'), - alignBottom: action('alignBottom'), - distributeHorizontally: action('distributeHorizontally'), - distributeVertically: action('distributeVertically'), }; storiesOf('components/Sidebar/SidebarHeader', module) diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx index 925e68a565f04..024a2dbb41a24 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx @@ -4,25 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component, Fragment } from 'react'; +import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiButtonIcon, - EuiContextMenu, - EuiToolTip, - EuiContextMenuPanelItemDescriptor, - EuiContextMenuPanelDescriptor, - EuiOverlayMask, -} from '@elastic/eui'; -import { Popover } from '../popover'; -import { CustomElementModal } from '../custom_element_modal'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { ToolTipShortcut } from '../tool_tip_shortcut/'; import { ComponentStrings } from '../../../i18n/components'; import { ShortcutStrings } from '../../../i18n/shortcuts'; -import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../common/lib/constants'; const { SidebarHeader: strings } = ComponentStrings; const shortcutHelp = ShortcutStrings.getShortcutHelp(); @@ -36,26 +23,6 @@ interface Props { * indicated whether or not layer controls should be displayed */ showLayerControls?: boolean; - /** - * cuts selected elements - */ - cutNodes: () => void; - /** - * copies selected elements to clipboard - */ - copyNodes: () => void; - /** - * pastes elements stored in clipboard to page - */ - pasteNodes: () => void; - /** - * clones selected elements - */ - cloneNodes: () => void; - /** - * deletes selected elements - */ - deleteNodes: () => void; /** * moves selected element to top layer */ @@ -72,493 +39,117 @@ interface Props { * moves selected element to bottom layer */ sendToBack: () => void; - /** - * saves the selected elements as an custom-element saved object - */ - createCustomElement: (name: string, description: string, image: string) => void; - /** - * indicated whether the selected element is a group or not - */ - groupIsSelected: boolean; - /** - * only more than one selected element can be grouped - */ - selectedNodes: string[]; - /** - * groups selected elements - */ - groupNodes: () => void; - /** - * ungroups selected group - */ - ungroupNodes: () => void; - /** - * left align selected elements - */ - alignLeft: () => void; - /** - * center align selected elements - */ - alignCenter: () => void; - /** - * right align selected elements - */ - alignRight: () => void; - /** - * top align selected elements - */ - alignTop: () => void; - /** - * middle align selected elements - */ - alignMiddle: () => void; - /** - * bottom align selected elements - */ - alignBottom: () => void; - /** - * horizontally distribute selected elements - */ - distributeHorizontally: () => void; - /** - * vertically distribute selected elements - */ - distributeVertically: () => void; -} - -interface State { - /** - * indicates whether or not the custom element modal is open - */ - isModalVisible: boolean; -} - -interface MenuTuple { - menuItem: EuiContextMenuPanelItemDescriptor; - panel: EuiContextMenuPanelDescriptor; } -const contextMenuButton = (handleClick: React.MouseEventHandler) => ( - -); - -export class SidebarHeader extends Component { - public static propTypes = { - title: PropTypes.string.isRequired, - showLayerControls: PropTypes.bool, // TODO: remove when we support relayering multiple elements - cutNodes: PropTypes.func.isRequired, - copyNodes: PropTypes.func.isRequired, - pasteNodes: PropTypes.func.isRequired, - cloneNodes: PropTypes.func.isRequired, - deleteNodes: PropTypes.func.isRequired, - bringToFront: PropTypes.func.isRequired, - bringForward: PropTypes.func.isRequired, - sendBackward: PropTypes.func.isRequired, - sendToBack: PropTypes.func.isRequired, - createCustomElement: PropTypes.func.isRequired, - groupIsSelected: PropTypes.bool, - selectedNodes: PropTypes.array, - groupNodes: PropTypes.func.isRequired, - ungroupNodes: PropTypes.func.isRequired, - alignLeft: PropTypes.func.isRequired, - alignCenter: PropTypes.func.isRequired, - alignRight: PropTypes.func.isRequired, - alignTop: PropTypes.func.isRequired, - alignMiddle: PropTypes.func.isRequired, - alignBottom: PropTypes.func.isRequired, - distributeHorizontally: PropTypes.func.isRequired, - distributeVertically: PropTypes.func.isRequired, - }; - - public static defaultProps = { - groupIsSelected: false, - showLayerControls: false, - selectedNodes: [], - }; - - public state = { - isModalVisible: false, - }; - - private _isMounted = false; - private _showModal = () => this._isMounted && this.setState({ isModalVisible: true }); - private _hideModal = () => this._isMounted && this.setState({ isModalVisible: false }); - - public componentDidMount() { - this._isMounted = true; - } - - public componentWillUnmount() { - this._isMounted = false; - } - - private _renderLayoutControls = () => { - const { bringToFront, bringForward, sendBackward, sendToBack } = this.props; - return ( - - - - {shortcutHelp.BRING_TO_FRONT} - - - } - > - - - - - - {shortcutHelp.BRING_FORWARD} - - - } - > - - - - - - {shortcutHelp.SEND_BACKWARD} - - - } - > - - - - - - {shortcutHelp.SEND_TO_BACK} - - - } - > - - - - - ); - }; - - private _getLayerMenuItems = (): MenuTuple => { - const { bringToFront, bringForward, sendBackward, sendToBack } = this.props; - - return { - menuItem: { - name: strings.getOrderMenuItemLabel(), - className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, - panel: 1, - }, - panel: { - id: 1, - title: strings.getOrderMenuItemLabel(), - items: [ - { - name: shortcutHelp.BRING_TO_FRONT, // TODO: check against current element position and disable if already top layer - icon: 'sortUp', - onClick: bringToFront, - }, - { - name: shortcutHelp.BRING_TO_FRONT, // TODO: same as above - icon: 'arrowUp', - onClick: bringForward, - }, - { - name: shortcutHelp.SEND_BACKWARD, // TODO: check against current element position and disable if already bottom layer - icon: 'arrowDown', - onClick: sendBackward, - }, - { - name: shortcutHelp.SEND_TO_BACK, // TODO: same as above - icon: 'sortDown', - onClick: sendToBack, - }, - ], - }, - }; - }; - - private _getAlignmentMenuItems = (close: (fn: () => void) => () => void): MenuTuple => { - const { alignLeft, alignCenter, alignRight, alignTop, alignMiddle, alignBottom } = this.props; - - return { - menuItem: { - name: strings.getAlignmentMenuItemLabel(), - className: 'canvasContextMenu', - panel: 2, - }, - panel: { - id: 2, - title: strings.getAlignmentMenuItemLabel(), - items: [ - { - name: strings.getLeftAlignMenuItemLabel(), - icon: 'editorItemAlignLeft', - onClick: close(alignLeft), - }, - { - name: strings.getCenterAlignMenuItemLabel(), - icon: 'editorItemAlignCenter', - onClick: close(alignCenter), - }, - { - name: strings.getRightAlignMenuItemLabel(), - icon: 'editorItemAlignRight', - onClick: close(alignRight), - }, - { - name: strings.getTopAlignMenuItemLabel(), - icon: 'editorItemAlignTop', - onClick: close(alignTop), - }, - { - name: strings.getMiddleAlignMenuItemLabel(), - icon: 'editorItemAlignMiddle', - onClick: close(alignMiddle), - }, - { - name: strings.getBottomAlignMenuItemLabel(), - icon: 'editorItemAlignBottom', - onClick: close(alignBottom), - }, - ], - }, - }; - }; - - private _getDistributionMenuItems = (close: (fn: () => void) => () => void): MenuTuple => { - const { distributeHorizontally, distributeVertically } = this.props; - - return { - menuItem: { - name: strings.getDistributionMenuItemLabel(), - className: 'canvasContextMenu', - panel: 3, - }, - panel: { - id: 3, - title: strings.getDistributionMenuItemLabel(), - items: [ - { - name: strings.getHorizontalDistributionMenuItemLabel(), - icon: 'editorDistributeHorizontal', - onClick: close(distributeHorizontally), - }, - { - name: strings.getVerticalDistributionMenuItemLabel(), - icon: 'editorDistributeVertical', - onClick: close(distributeVertically), - }, - ], - }, - }; - }; - - private _getGroupMenuItems = ( - close: (fn: () => void) => () => void - ): EuiContextMenuPanelItemDescriptor[] => { - const { groupIsSelected, ungroupNodes, groupNodes, selectedNodes } = this.props; - return groupIsSelected - ? [ - { - name: strings.getUngroupMenuItemLabel(), - className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, - onClick: close(ungroupNodes), - }, - ] - : selectedNodes.length > 1 - ? [ - { - name: strings.getGroupMenuItemLabel(), - className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, - onClick: close(groupNodes), - }, - ] - : []; - }; - - private _getPanels = (closePopover: () => void): EuiContextMenuPanelDescriptor[] => { - const { - showLayerControls, - cutNodes, - copyNodes, - pasteNodes, - deleteNodes, - cloneNodes, - } = this.props; - - // closes popover after invoking fn - const close = (fn: () => void) => () => { - fn(); - closePopover(); - }; - - const items: EuiContextMenuPanelItemDescriptor[] = [ - { - name: shortcutHelp.CUT, - icon: 'cut', - onClick: close(cutNodes), - }, - { - name: shortcutHelp.COPY, - icon: 'copy', - onClick: copyNodes, - }, - { - name: shortcutHelp.PASTE, // TODO: can this be disabled if clipboard is empty? - icon: 'copyClipboard', - onClick: close(pasteNodes), - }, - { - name: shortcutHelp.DELETE, - icon: 'trash', - onClick: close(deleteNodes), - }, - { - name: shortcutHelp.CLONE, - onClick: close(cloneNodes), - }, - ...this._getGroupMenuItems(close), - ]; - - const panels: EuiContextMenuPanelDescriptor[] = [ - { - id: 0, - title: strings.getContextMenuTitle(), - items, - }, - ]; - - const fillMenu = ({ menuItem, panel }: MenuTuple) => { - items.push(menuItem); // add Order menu item to first panel - panels.push(panel); // add nested panel for layers controls - }; - - if (showLayerControls) { - fillMenu(this._getLayerMenuItems()); - } - - if (this.props.selectedNodes.length > 1) { - fillMenu(this._getAlignmentMenuItems(close)); - } - - if (this.props.selectedNodes.length > 2) { - fillMenu(this._getDistributionMenuItems(close)); - } - - items.push({ - name: strings.getSaveElementMenuItemLabel(), - icon: 'indexOpen', - className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, - onClick: this._showModal, - }); - - return panels; - }; - - private _renderContextMenu = () => ( - - {({ closePopover }: { closePopover: () => void }) => ( - - )} - - ); - - private _handleSave = (name: string, description: string, image: string) => { - const { createCustomElement } = this.props; - createCustomElement(name, description, image); - this._hideModal(); - }; - - render() { - const { title, showLayerControls } = this.props; - const { isModalVisible } = this.state; - - return ( - - +export const SidebarHeader: FunctionComponent = ({ + title, + showLayerControls, + bringToFront, + bringForward, + sendBackward, + sendToBack, +}) => ( + + + +

    {title}

    +
    +
    + {showLayerControls ? ( + + - -

    {title}

    -
    + + {shortcutHelp.BRING_TO_FRONT} + + + } + > + +
    - - {showLayerControls ? this._renderLayoutControls() : null} - - - - - - {this._renderContextMenu()} - + + {shortcutHelp.BRING_FORWARD} + + + } + > + + + + + + {shortcutHelp.SEND_BACKWARD} + + + } + > + + + + + + {shortcutHelp.SEND_TO_BACK} + + + } + > + +
    - {isModalVisible ? ( - - - - ) : null} -
    - ); - } -} +
    + ) : null} +
    +); + +SidebarHeader.propTypes = { + title: PropTypes.string.isRequired, + showLayerControls: PropTypes.bool, // TODO: remove when we support relayering multiple elements + bringToFront: PropTypes.func.isRequired, + bringForward: PropTypes.func.isRequired, + sendBackward: PropTypes.func.isRequired, + sendToBack: PropTypes.func.isRequired, +}; + +SidebarHeader.defaultProps = { + showLayerControls: false, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.examples.storyshot new file mode 100644 index 0000000000000..42c59d41dca62 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.examples.storyshot @@ -0,0 +1,205 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadHeader/EditMenu 2 elements selected 1`] = ` +
    +
    + +
    +
    +`; + +exports[`Storyshots components/WorkpadHeader/EditMenu 3+ elements selected 1`] = ` +
    +
    + +
    +
    +`; + +exports[`Storyshots components/WorkpadHeader/EditMenu clipboard data exists 1`] = ` +
    +
    + +
    +
    +`; + +exports[`Storyshots components/WorkpadHeader/EditMenu default 1`] = ` +
    +
    + +
    +
    +`; + +exports[`Storyshots components/WorkpadHeader/EditMenu single element selected 1`] = ` +
    +
    + +
    +
    +`; + +exports[`Storyshots components/WorkpadHeader/EditMenu single grouped element selected 1`] = ` +
    +
    + +
    +
    +`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx new file mode 100644 index 0000000000000..a0ab8d53485f5 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx @@ -0,0 +1,69 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { EditMenu } from '../edit_menu'; + +const handlers = { + cutNodes: action('cutNodes'), + copyNodes: action('copyNodes'), + pasteNodes: action('pasteNodes'), + deleteNodes: action('deleteNodes'), + cloneNodes: action('cloneNodes'), + bringToFront: action('bringToFront'), + bringForward: action('bringForward'), + sendBackward: action('sendBackward'), + sendToBack: action('sendToBack'), + alignLeft: action('alignLeft'), + alignCenter: action('alignCenter'), + alignRight: action('alignRight'), + alignTop: action('alignTop'), + alignMiddle: action('alignMiddle'), + alignBottom: action('alignBottom'), + distributeHorizontally: action('distributeHorizontally'), + distributeVertically: action('distributeVertically'), + createCustomElement: action('createCustomElement'), + groupNodes: action('groupNodes'), + ungroupNodes: action('ungroupNodes'), + undoHistory: action('undoHistory'), + redoHistory: action('redoHistory'), +}; + +storiesOf('components/WorkpadHeader/EditMenu', module) + .add('default', () => ( + + )) + .add('clipboard data exists', () => ( + + )) + .add('single element selected', () => ( + + )) + .add('single grouped element selected', () => ( + + )) + .add('2 elements selected', () => ( + + )) + .add('3+ elements selected', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx new file mode 100644 index 0000000000000..15191b8d416ff --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx @@ -0,0 +1,448 @@ +/* + * 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, { Fragment, FunctionComponent, useState } from 'react'; +import PropTypes from 'prop-types'; +import { EuiButtonEmpty, EuiContextMenu, EuiIcon, EuiOverlayMask } from '@elastic/eui'; +import { ComponentStrings } from '../../../../i18n/components'; +import { ShortcutStrings } from '../../../../i18n/shortcuts'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { Popover, ClosePopoverFn } from '../../popover'; +import { CustomElementModal } from '../../custom_element_modal'; +import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib/constants'; + +const { WorkpadHeaderEditMenu: strings } = ComponentStrings; +const shortcutHelp = ShortcutStrings.getShortcutHelp(); + +export interface Props { + /** + * cuts selected elements + */ + cutNodes: () => void; + /** + * copies selected elements to clipboard + */ + copyNodes: () => void; + /** + * pastes elements stored in clipboard to page + */ + pasteNodes: () => void; + /** + * clones selected elements + */ + cloneNodes: () => void; + /** + * deletes selected elements + */ + deleteNodes: () => void; + /** + * moves selected element to top layer + */ + bringToFront: () => void; + /** + * moves selected element up one layer + */ + bringForward: () => void; + /** + * moves selected element down one layer + */ + sendBackward: () => void; + /** + * moves selected element to bottom layer + */ + sendToBack: () => void; + /** + * saves the selected elements as an custom-element saved object + */ + createCustomElement: (name: string, description: string, image: string) => void; + /** + * indicated whether the selected element is a group or not + */ + groupIsSelected: boolean; + /** + * only more than one selected element can be grouped + */ + selectedNodes: string[]; + /** + * groups selected elements + */ + groupNodes: () => void; + /** + * ungroups selected group + */ + ungroupNodes: () => void; + /** + * left align selected elements + */ + alignLeft: () => void; + /** + * center align selected elements + */ + alignCenter: () => void; + /** + * right align selected elements + */ + alignRight: () => void; + /** + * top align selected elements + */ + alignTop: () => void; + /** + * middle align selected elements + */ + alignMiddle: () => void; + /** + * bottom align selected elements + */ + alignBottom: () => void; + /** + * horizontally distribute selected elements + */ + distributeHorizontally: () => void; + /** + * vertically distribute selected elements + */ + distributeVertically: () => void; + /** + * Reverts last change to the workpad + */ + undoHistory: () => void; + /** + * Reapplies last reverted change to the workpad + */ + redoHistory: () => void; + /** + * Is there element clipboard data to paste? + */ + hasPasteData: boolean; +} + +export const EditMenu: FunctionComponent = ({ + cutNodes, + copyNodes, + pasteNodes, + deleteNodes, + cloneNodes, + bringToFront, + bringForward, + sendBackward, + sendToBack, + alignLeft, + alignCenter, + alignRight, + alignTop, + alignMiddle, + alignBottom, + distributeHorizontally, + distributeVertically, + createCustomElement, + selectedNodes, + groupIsSelected, + groupNodes, + ungroupNodes, + undoHistory, + redoHistory, + hasPasteData, +}) => { + const [isModalVisible, setModalVisible] = useState(false); + const showModal = () => setModalVisible(true); + const hideModal = () => setModalVisible(false); + + const handleSave = (name: string, description: string, image: string) => { + createCustomElement(name, description, image); + hideModal(); + }; + + const editControl = (togglePopover: React.MouseEventHandler) => ( + + {strings.getEditMenuButtonLabel()} + + ); + + const getPanelTree = (closePopover: ClosePopoverFn) => { + const groupMenuItem = groupIsSelected + ? { + name: strings.getUngroupMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + icon: , + onClick: () => { + ungroupNodes(); + closePopover(); + }, + } + : { + name: strings.getGroupMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + icon: , + disabled: selectedNodes.length < 2, + onClick: () => { + groupNodes(); + closePopover(); + }, + }; + + const orderMenuItem = { + name: strings.getOrderMenuItemLabel(), + disabled: selectedNodes.length !== 1, // TODO: change to === 0 when we support relayering multiple elements + icon: , + panel: { + id: 1, + title: strings.getOrderMenuItemLabel(), + items: [ + { + name: shortcutHelp.BRING_TO_FRONT, // TODO: check against current element position and disable if already top layer + icon: 'sortUp', + onClick: bringToFront, + }, + { + name: shortcutHelp.BRING_FORWARD, // TODO: same as above + icon: 'arrowUp', + onClick: bringForward, + }, + { + name: shortcutHelp.SEND_BACKWARD, // TODO: check against current element position and disable if already bottom layer + icon: 'arrowDown', + onClick: sendBackward, + }, + { + name: shortcutHelp.SEND_TO_BACK, // TODO: same as above + icon: 'sortDown', + onClick: sendToBack, + }, + ], + }, + }; + + const alignmentMenuItem = { + name: strings.getAlignmentMenuItemLabel(), + className: 'canvasContextMenu', + disabled: groupIsSelected || selectedNodes.length < 2, + icon: , + panel: { + id: 2, + title: strings.getAlignmentMenuItemLabel(), + items: [ + { + name: strings.getLeftAlignMenuItemLabel(), + icon: 'editorItemAlignLeft', + onClick: () => { + alignLeft(); + closePopover(); + }, + }, + { + name: strings.getCenterAlignMenuItemLabel(), + icon: 'editorItemAlignCenter', + onClick: () => { + alignCenter(); + closePopover(); + }, + }, + { + name: strings.getRightAlignMenuItemLabel(), + icon: 'editorItemAlignRight', + onClick: () => { + alignRight(); + closePopover(); + }, + }, + { + name: strings.getTopAlignMenuItemLabel(), + icon: 'editorItemAlignTop', + onClick: () => { + alignTop(); + closePopover(); + }, + }, + { + name: strings.getMiddleAlignMenuItemLabel(), + icon: 'editorItemAlignMiddle', + onClick: () => { + alignMiddle(); + closePopover(); + }, + }, + { + name: strings.getBottomAlignMenuItemLabel(), + icon: 'editorItemAlignBottom', + onClick: () => { + alignBottom(); + closePopover(); + }, + }, + ], + }, + }; + + const distributionMenuItem = { + name: strings.getDistributionMenuItemLabel(), + className: 'canvasContextMenu', + disabled: groupIsSelected || selectedNodes.length < 3, + icon: , + panel: { + id: 3, + title: strings.getAlignmentMenuItemLabel(), + items: [ + { + name: strings.getHorizontalDistributionMenuItemLabel(), + icon: 'editorDistributeHorizontal', + onClick: () => { + distributeHorizontally(); + closePopover(); + }, + }, + { + name: strings.getVerticalDistributionMenuItemLabel(), + icon: 'editorDistributeVertical', + onClick: () => { + distributeVertically(); + closePopover(); + }, + }, + ], + }, + }; + + const savedElementMenuItem = { + name: strings.getSaveElementMenuItemLabel(), + icon: , + disabled: selectedNodes.length < 1, + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + 'data-test-subj': 'canvasWorkpadEditMenu__saveElementButton', + onClick: () => { + showModal(); + closePopover(); + }, + }; + + const items = [ + { + // TODO: check history and disable when there are no more changes to revert + name: strings.getUndoMenuItemLabel(), + icon: , + onClick: () => { + undoHistory(); + }, + }, + { + // TODO: check history and disable when there are no more changes to reapply + name: strings.getRedoMenuItemLabel(), + icon: , + onClick: () => { + redoHistory(); + }, + }, + { + name: shortcutHelp.CUT, + icon: , + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + disabled: selectedNodes.length < 1, + onClick: () => { + cutNodes(); + closePopover(); + }, + }, + { + name: shortcutHelp.COPY, + disabled: selectedNodes.length < 1, + icon: , + onClick: () => { + copyNodes(); + }, + }, + { + name: shortcutHelp.PASTE, // TODO: can this be disabled if clipboard is empty? + icon: , + disabled: !hasPasteData, + onClick: () => { + pasteNodes(); + closePopover(); + }, + }, + { + name: shortcutHelp.DELETE, + icon: , + disabled: selectedNodes.length < 1, + onClick: () => { + deleteNodes(); + closePopover(); + }, + }, + { + name: shortcutHelp.CLONE, + icon: , + disabled: selectedNodes.length < 1, + onClick: () => { + cloneNodes(); + closePopover(); + }, + }, + groupMenuItem, + orderMenuItem, + alignmentMenuItem, + distributionMenuItem, + savedElementMenuItem, + ]; + + return { + id: 0, + // title: strings.getEditMenuLabel(), + items, + }; + }; + + return ( + + + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + + )} + + {isModalVisible ? ( + + + + ) : null} + + ); +}; + +EditMenu.propTypes = { + cutNodes: PropTypes.func.isRequired, + copyNodes: PropTypes.func.isRequired, + pasteNodes: PropTypes.func.isRequired, + deleteNodes: PropTypes.func.isRequired, + cloneNodes: PropTypes.func.isRequired, + bringToFront: PropTypes.func.isRequired, + bringForward: PropTypes.func.isRequired, + sendBackward: PropTypes.func.isRequired, + sendToBack: PropTypes.func.isRequired, + alignLeft: PropTypes.func.isRequired, + alignCenter: PropTypes.func.isRequired, + alignRight: PropTypes.func.isRequired, + alignTop: PropTypes.func.isRequired, + alignMiddle: PropTypes.func.isRequired, + alignBottom: PropTypes.func.isRequired, + distributeHorizontally: PropTypes.func.isRequired, + distributeVertically: PropTypes.func.isRequired, + createCustomElement: PropTypes.func.isRequired, + selectedNodes: PropTypes.arrayOf(PropTypes.string).isRequired, + groupIsSelected: PropTypes.bool.isRequired, + groupNodes: PropTypes.func.isRequired, + ungroupNodes: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/index.ts new file mode 100644 index 0000000000000..a8bb7177dbd24 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/index.ts @@ -0,0 +1,123 @@ +/* + * 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 { connect } from 'react-redux'; +import { compose, withHandlers, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { State, PositionedElement } from '../../../../types'; +import { getClipboardData } from '../../../lib/clipboard'; +// @ts-ignore Untyped local +import { flatten } from '../../../lib/aeroelastic/functional'; +// @ts-ignore Untyped local +import { globalStateUpdater } from '../../workpad_page/integration_utils'; +// @ts-ignore Untyped local +import { crawlTree } from '../../workpad_page/integration_utils'; +// @ts-ignore Untyped local +import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements'; +// @ts-ignore Untyped local +import { undoHistory, redoHistory } from '../../../state/actions/history'; +// @ts-ignore Untyped local +import { selectToplevelNodes } from '../../../state/actions/transient'; +import { + getSelectedPage, + getNodes, + getSelectedToplevelNodes, +} from '../../../state/selectors/workpad'; +import { + layerHandlerCreators, + clipboardHandlerCreators, + basicHandlerCreators, + groupHandlerCreators, + alignmentDistributionHandlerCreators, +} from '../../../lib/element_handler_creators'; +import { EditMenu as Component, Props as ComponentProps } from './edit_menu'; + +type LayoutState = any; + +type CommitFn = (type: string, payload: any) => LayoutState; + +interface OwnProps { + commit: CommitFn; +} + +const withGlobalState = ( + commit: CommitFn, + updateGlobalState: (layoutState: LayoutState) => void +) => (type: string, payload: any) => { + const newLayoutState = commit(type, payload); + if (newLayoutState.currentScene.gestureEnd) { + updateGlobalState(newLayoutState); + } +}; + +/* + * TODO: this is all copied from interactive_workpad_page and workpad_shortcuts + */ +const mapStateToProps = (state: State) => { + const pageId = getSelectedPage(state); + const nodes = getNodes(state, pageId) as PositionedElement[]; + const selectedToplevelNodes = getSelectedToplevelNodes(state); + const selectedPrimaryShapeObjects = selectedToplevelNodes + .map((id: string) => nodes.find((s: PositionedElement) => s.id === id)) + .filter((shape?: PositionedElement) => shape) as PositionedElement[]; + const selectedPersistentPrimaryNodes = flatten( + selectedPrimaryShapeObjects.map((shape: PositionedElement) => + nodes.find((n: PositionedElement) => n.id === shape.id) // is it a leaf or a persisted group? + ? [shape.id] + : nodes.filter((s: PositionedElement) => s.position.parent === shape.id).map(s => s.id) + ) + ); + const selectedNodeIds = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); + + return { + pageId, + selectedToplevelNodes, + selectedNodes: selectedNodeIds.map((id: string) => nodes.find(s => s.id === id)), + state, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), + removeNodes: (nodeIds: string[], pageId: string) => dispatch(removeElements(nodeIds, pageId)), + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes(nodes.filter((e: PositionedElement) => !e.position.parent).map(e => e.id)) + ), + elementLayer: (pageId: string, elementId: string, movement: number) => { + dispatch(elementLayer({ pageId, elementId, movement })); + }, + undoHistory: () => dispatch(undoHistory()), + redoHistory: () => dispatch(redoHistory()), + dispatch, +}); + +const mergeProps = ( + { state, selectedToplevelNodes, ...restStateProps }: ReturnType, + { dispatch, ...restDispatchProps }: ReturnType, + { commit }: OwnProps +) => { + const updateGlobalState = globalStateUpdater(dispatch, state); + + return { + ...restDispatchProps, + ...restStateProps, + commit: withGlobalState(commit, updateGlobalState), + groupIsSelected: + selectedToplevelNodes.length === 1 && selectedToplevelNodes[0].includes('group'), + }; +}; + +export const EditMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })), + withHandlers(basicHandlerCreators), + withHandlers(clipboardHandlerCreators), + withHandlers(layerHandlerCreators), + withHandlers(groupHandlerCreators), + withHandlers(alignmentDistributionHandlerCreators) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx index 5c420cf3a04c9..fbb5d70dfc55c 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx @@ -139,7 +139,6 @@ export const ElementMenu: FunctionComponent = ({ return { id: 0, - title: strings.getElementMenuLabel(), items: [ elementListToMenuItems(textElements), elementListToMenuItems(shapeElements), diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx index 621077c29c368..2ac0591a1bdd4 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx @@ -62,7 +62,6 @@ export const ShareMenu: FunctionComponent = ({ onCopy, onExport, getExpor const getPanelTree = (closePopover: ClosePopoverFn) => ({ id: 0, - title: strings.getShareWorkpadMessage(), items: [ { name: strings.getShareDownloadJSONTitle(), diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx index 1f8e47b556771..4aab8280a9f24 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx @@ -15,6 +15,7 @@ import { ToolTipShortcut } from '../tool_tip_shortcut/'; import { RefreshControl } from './refresh_control'; // @ts-ignore untyped local import { FullscreenControl } from './fullscreen_control'; +import { EditMenu } from './edit_menu'; import { ElementMenu } from './element_menu'; import { ShareMenu } from './share_menu'; import { ViewMenu } from './view_menu'; @@ -25,12 +26,14 @@ export interface Props { isWriteable: boolean; toggleWriteable: () => void; canUserWrite: boolean; + commit: (type: string, payload: any) => any; } export const WorkpadHeader: FunctionComponent = ({ isWriteable, canUserWrite, toggleWriteable, + commit, }) => { const keyHandler = (action: string) => { if (action === 'EDITING') { @@ -99,6 +102,9 @@ export const WorkpadHeader: FunctionComponent = ({ + + + diff --git a/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts index 1623035bd25ba..80a7c34e8bef5 100644 --- a/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts @@ -363,7 +363,11 @@ export function getNodesForPage(page: CanvasPage, withAst: boolean): CanvasEleme } // todo unify or DRY up with `getElements` -export function getNodes(state: State, pageId: string, withAst = true): CanvasElement[] { +export function getNodes( + state: State, + pageId: string, + withAst = true +): CanvasElement[] | PositionedElement[] { const id = pageId || getSelectedPage(state); if (!id) { return []; diff --git a/x-pack/legacy/plugins/canvas/types/elements.ts b/x-pack/legacy/plugins/canvas/types/elements.ts index 86356f5bd32a9..5de6b4968545f 100644 --- a/x-pack/legacy/plugins/canvas/types/elements.ts +++ b/x-pack/legacy/plugins/canvas/types/elements.ts @@ -75,4 +75,8 @@ export interface ElementPosition { parent: string | null; } -export type PositionedElement = CanvasElement & { ast: ExpressionAstExpression }; +export type PositionedElement = CanvasElement & { + ast: ExpressionAstExpression; +} & { + position: ElementPosition; +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 49a749c578caf..d1270ea92c51e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5521,26 +5521,10 @@ "xpack.canvas.sidebarContent.groupedElementSidebarTitle": "グループ化されたエレメント", "xpack.canvas.sidebarContent.multiElementSidebarTitle": "複数エレメント", "xpack.canvas.sidebarContent.singleElementSidebarTitle": "選択されたエレメント", - "xpack.canvas.sidebarHeader.alignmentMenuItemLabel": "アラインメント", - "xpack.canvas.sidebarHeader.bottomAlignMenuItemLabel": "一番下", "xpack.canvas.sidebarHeader.bringForwardArialLabel": "エレメントを 1 つ上のレイヤーに移動", "xpack.canvas.sidebarHeader.bringToFrontArialLabel": "エレメントを一番上のレイヤーに移動", - "xpack.canvas.sidebarHeader.centerAlignMenuItemLabel": "中央", - "xpack.canvas.sidebarHeader.contextMenuAriaLabel": "エレメントオプション", - "xpack.canvas.sidebarHeader.createElementModalTitle": "新規エレメントの作成", - "xpack.canvas.sidebarHeader.distributionMenutItemLabel": "分布", - "xpack.canvas.sidebarHeader.groupMenuItemLabel": "グループ", - "xpack.canvas.sidebarHeader.horizontalDistributionMenutItemLabel": "横", - "xpack.canvas.sidebarHeader.leftAlignMenuItemLabel": "左", - "xpack.canvas.sidebarHeader.middleAlignMenuItemLabel": "真ん中", - "xpack.canvas.sidebarHeader.orderMenuItemLabel": "順序", - "xpack.canvas.sidebarHeader.rightAlignMenuItemLabel": "右", - "xpack.canvas.sidebarHeader.savedElementMenuItemLabel": "新規エレメントとして保存", "xpack.canvas.sidebarHeader.sendBackwardArialLabel": "エレメントを 1 つ下のレイヤーに移動", "xpack.canvas.sidebarHeader.sendToBackArialLabel": "エレメントを一番下のレイヤーに移動", - "xpack.canvas.sidebarHeader.topAlignMenuItemLabel": "一番上", - "xpack.canvas.sidebarHeader.ungroupMenuItemLabel": "グループ解除", - "xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel": "縦", "xpack.canvas.tags.presentationTag": "プレゼンテーション", "xpack.canvas.tags.reportTag": "レポート", "xpack.canvas.templates.darkHelp": "ダークカラーテーマのプレゼンテーションデッキです", @@ -5854,6 +5838,18 @@ "xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel": "設定", "xpack.canvas.workpadHeaderCustomInterval.formDescription": "{secondsExample}、{minutesExample}、{hoursExample} のような短い表記を使用します", "xpack.canvas.workpadHeaderCustomInterval.formLabel": "カスタム間隔を設定", + "xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel": "アラインメント", + "xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel": "一番下", + "xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel": "中央", + "xpack.canvas.workpadHeaderEditMenu.createElementModalTitle": "新規エレメントの作成", + "xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel": "分布", + "xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel": "グループ", + "xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel": "横", + "xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel": "左", + "xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel": "真ん中", + "xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel": "順序", + "xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel": "右", + "xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel": "新規エレメントとして保存", "xpack.canvas.workpadHeaderKioskControl.controlTitle": "全画面ページのサイクル", "xpack.canvas.workpadHeaderKioskControl.cycleFormLabel": "サイクル間隔を変更", "xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch": "スライドを自動的にサイクル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index adb8ac1817a14..32c91a6ef2931 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5522,26 +5522,10 @@ "xpack.canvas.sidebarContent.groupedElementSidebarTitle": "已分组元素", "xpack.canvas.sidebarContent.multiElementSidebarTitle": "多个元素", "xpack.canvas.sidebarContent.singleElementSidebarTitle": "选定元素", - "xpack.canvas.sidebarHeader.alignmentMenuItemLabel": "对齐方式", - "xpack.canvas.sidebarHeader.bottomAlignMenuItemLabel": "下", "xpack.canvas.sidebarHeader.bringForwardArialLabel": "将元素上移一层", "xpack.canvas.sidebarHeader.bringToFrontArialLabel": "将元素移到顶层", - "xpack.canvas.sidebarHeader.centerAlignMenuItemLabel": "中", - "xpack.canvas.sidebarHeader.contextMenuAriaLabel": "元素选项", - "xpack.canvas.sidebarHeader.createElementModalTitle": "创建新元素", - "xpack.canvas.sidebarHeader.distributionMenutItemLabel": "分布", - "xpack.canvas.sidebarHeader.groupMenuItemLabel": "分组", - "xpack.canvas.sidebarHeader.horizontalDistributionMenutItemLabel": "水平", - "xpack.canvas.sidebarHeader.leftAlignMenuItemLabel": "左", - "xpack.canvas.sidebarHeader.middleAlignMenuItemLabel": "中", - "xpack.canvas.sidebarHeader.orderMenuItemLabel": "顺序", - "xpack.canvas.sidebarHeader.rightAlignMenuItemLabel": "右", - "xpack.canvas.sidebarHeader.savedElementMenuItemLabel": "另存为新元素", "xpack.canvas.sidebarHeader.sendBackwardArialLabel": "将元素下移一层", "xpack.canvas.sidebarHeader.sendToBackArialLabel": "将元素移到底层", - "xpack.canvas.sidebarHeader.topAlignMenuItemLabel": "上", - "xpack.canvas.sidebarHeader.ungroupMenuItemLabel": "取消分组", - "xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel": "垂直", "xpack.canvas.tags.presentationTag": "演示", "xpack.canvas.tags.reportTag": "报告", "xpack.canvas.templates.darkHelp": "深色主题的演示幻灯片", @@ -5856,6 +5840,21 @@ "xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel": "设置", "xpack.canvas.workpadHeaderCustomInterval.formDescription": "使用速记表示法,如 {secondsExample}、{minutesExample} 或 {hoursExample}", "xpack.canvas.workpadHeaderCustomInterval.formLabel": "设置定制时间间隔", + "xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel": "对齐方式", + "xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel": "下", + "xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel": "中", + "xpack.canvas.workpadHeaderEditMenu.createElementModalTitle": "创建新元素", + "xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel": "分布", + "xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel": "分组", + "xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel": "水平", + "xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel": "左", + "xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel": "中", + "xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel": "顺序", + "xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel": "右", + "xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel": "另存为新元素", + "xpack.canvas.workpadHeaderEditMenu.topAlignMenuItemLabel": "上", + "xpack.canvas.workpadHeaderEditMenu.ungroupMenuItemLabel": "取消分组", + "xpack.canvas.workpadHeaderEditMenu.verticalDistributionMenutItemLabel": "垂直", "xpack.canvas.workpadHeaderKioskControl.controlTitle": "循环播放全屏页面", "xpack.canvas.workpadHeaderKioskControl.cycleFormLabel": "更改循环播放时间间隔", "xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch": "自动循环播放幻灯片", diff --git a/x-pack/test/functional/apps/canvas/custom_elements.ts b/x-pack/test/functional/apps/canvas/custom_elements.ts index d4e1702368879..3bd3749ec6935 100644 --- a/x-pack/test/functional/apps/canvas/custom_elements.ts +++ b/x-pack/test/functional/apps/canvas/custom_elements.ts @@ -40,8 +40,11 @@ export default function canvasCustomElementTest({ // find the first workpad element (a markdown element) and click it to select it await testSubjects.click('canvasWorkpadPage > canvasWorkpadPageElementContent', 20000); + // click "Edit" menu + await testSubjects.click('canvasWorkpadEditMenuButton', 20000); + // click the "Save as new element" button - await testSubjects.click('canvasSidebarHeader__saveElementButton', 20000); + await testSubjects.click('canvasWorkpadEditMenu__saveElementButton', 20000); // fill out the custom element form and submit it await PageObjects.canvas.fillOutCustomElementForm( From 3cef8e6f309f8d52b7b9c5553692f52a4131e815 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Thu, 30 Apr 2020 16:53:56 -0500 Subject: [PATCH 057/122] [Metrics UI] Add inventory metric threshold alerts (#64292) * Add new inventory metric threshold alert * Add missed file * Fix some types * Convert units on client and executor. * Move formatters to common. Properly format metrics in alert messages * Style changes * Remove unused files * fix test * Update create * Fix signature * Remove old test. Remove unecessary import * Pass in filter when clicking create alert from context menu * Fix filtering * Fix more types * Fix tests * Fix merge * Fix merge Co-authored-by: Elastic Machine --- .../utils => common}/formatters/bytes.test.ts | 4 +- .../utils => common}/formatters/bytes.ts | 3 +- .../utils => common}/formatters/datetime.ts | 0 .../formatters/high_precision.ts | 0 .../utils => common}/formatters/index.ts | 4 +- .../utils => common}/formatters/number.ts | 0 .../utils => common}/formatters/percent.ts | 0 .../formatters/snapshot_metric_formats.ts | 73 +++ .../plugins/infra/common/formatters/types.ts | 11 + .../aws_ec2/toolbar_items.tsx | 32 +- .../aws_rds/toolbar_items.tsx | 30 +- .../inventory_models/aws_s3/toolbar_items.tsx | 22 +- .../aws_sqs/toolbar_items.tsx | 21 +- .../container/toolbar_items.tsx | 23 +- .../inventory_models/host/toolbar_items.tsx | 27 +- .../inventory_models/pod/toolbar_items.tsx | 9 +- .../components/alert_dropdown.tsx | 2 +- .../components/expression.tsx | 37 +- .../alerting/inventory/alert_dropdown.tsx | 62 +++ .../alerting/inventory/alert_flyout.tsx | 52 ++ .../alerting/inventory/expression.tsx | 498 ++++++++++++++++++ .../components/alerting/inventory/metric.tsx | 150 ++++++ .../metric_inventory_threshold_alert_type.ts | 34 ++ .../alerting/inventory/node_type.tsx | 115 ++++ .../alerting/inventory/validation.tsx | 80 +++ .../log_text_stream/column_headers.tsx | 2 +- .../logging/log_text_stream/log_date_row.tsx | 2 +- .../containers/source/use_source_via_http.ts | 17 +- x-pack/plugins/infra/public/index.ts | 2 +- x-pack/plugins/infra/public/lib/lib.ts | 6 - .../infra/public/pages/metrics/index.tsx | 7 +- .../components/waffle/node_context_menu.tsx | 315 ++++++----- .../lib/create_inventory_metric_formatter.ts | 2 +- .../components/gauges_section_vis.tsx | 2 +- .../metric_detail/components/helpers.ts | 2 +- .../helpers/create_formatter_for_metric.ts | 2 +- .../metrics_explorer/components/kuery_bar.tsx | 5 + x-pack/plugins/infra/public/plugin.ts | 2 + .../infra/server/graphql/sources/resolvers.ts | 10 +- .../metrics/kibana_metrics_adapter.ts | 10 +- .../inventory_metric_threshold_executor.ts | 214 ++++++++ ...r_inventory_metric_threshold_alert_type.ts | 92 ++++ .../inventory_metric_threshold/types.ts | 35 ++ .../metric_threshold_executor.test.ts | 192 ++++--- .../metric_threshold_executor.ts | 63 +-- .../register_metric_threshold_alert_type.ts | 39 +- .../lib/alerting/register_alert_types.ts | 5 +- .../infra/server/lib/compose/kibana.ts | 2 +- .../create_timerange_with_interval.ts | 21 +- .../infra/server/lib/snapshot/snapshot.ts | 52 +- .../infra/server/lib/sources/sources.ts | 33 +- x-pack/plugins/infra/server/plugin.ts | 2 +- .../routes/log_sources/configuration.ts | 4 +- .../lib/get_dataset_for_field.ts | 13 +- .../lib/populate_series_with_tsvb_data.ts | 16 +- .../infra/server/routes/snapshot/index.ts | 9 +- .../server/utils/calculate_metric_interval.ts | 14 +- .../apis/infra/metrics_alerting.ts | 11 +- 58 files changed, 1995 insertions(+), 497 deletions(-) rename x-pack/plugins/infra/{public/utils => common}/formatters/bytes.test.ts (93%) rename x-pack/plugins/infra/{public/utils => common}/formatters/bytes.ts (96%) rename x-pack/plugins/infra/{public/utils => common}/formatters/datetime.ts (100%) rename x-pack/plugins/infra/{public/utils => common}/formatters/high_precision.ts (100%) rename x-pack/plugins/infra/{public/utils => common}/formatters/index.ts (90%) rename x-pack/plugins/infra/{public/utils => common}/formatters/number.ts (100%) rename x-pack/plugins/infra/{public/utils => common}/formatters/percent.ts (100%) create mode 100644 x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts create mode 100644 x-pack/plugins/infra/common/formatters/types.ts create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts create mode 100644 x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts diff --git a/x-pack/plugins/infra/public/utils/formatters/bytes.test.ts b/x-pack/plugins/infra/common/formatters/bytes.test.ts similarity index 93% rename from x-pack/plugins/infra/public/utils/formatters/bytes.test.ts rename to x-pack/plugins/infra/common/formatters/bytes.test.ts index 4c872bcee057d..ccdeed120acca 100644 --- a/x-pack/plugins/infra/public/utils/formatters/bytes.test.ts +++ b/x-pack/plugins/infra/common/formatters/bytes.test.ts @@ -3,9 +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 { InfraWaffleMapDataFormat } from '../../lib/lib'; +import { InfraWaffleMapDataFormat } from './types'; import { createBytesFormatter } from './bytes'; + describe('createDataFormatter', () => { it('should format bytes as bytesDecimal', () => { const formatter = createBytesFormatter(InfraWaffleMapDataFormat.bytesDecimal); diff --git a/x-pack/plugins/infra/public/utils/formatters/bytes.ts b/x-pack/plugins/infra/common/formatters/bytes.ts similarity index 96% rename from x-pack/plugins/infra/public/utils/formatters/bytes.ts rename to x-pack/plugins/infra/common/formatters/bytes.ts index 80a5603ed6994..3a45caa8b5e15 100644 --- a/x-pack/plugins/infra/public/utils/formatters/bytes.ts +++ b/x-pack/plugins/infra/common/formatters/bytes.ts @@ -3,9 +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 { InfraWaffleMapDataFormat } from '../../lib/lib'; import { formatNumber } from './number'; +import { InfraWaffleMapDataFormat } from './types'; /** * The labels are derived from these two Wikipedia articles. diff --git a/x-pack/plugins/infra/public/utils/formatters/datetime.ts b/x-pack/plugins/infra/common/formatters/datetime.ts similarity index 100% rename from x-pack/plugins/infra/public/utils/formatters/datetime.ts rename to x-pack/plugins/infra/common/formatters/datetime.ts diff --git a/x-pack/plugins/infra/public/utils/formatters/high_precision.ts b/x-pack/plugins/infra/common/formatters/high_precision.ts similarity index 100% rename from x-pack/plugins/infra/public/utils/formatters/high_precision.ts rename to x-pack/plugins/infra/common/formatters/high_precision.ts diff --git a/x-pack/plugins/infra/public/utils/formatters/index.ts b/x-pack/plugins/infra/common/formatters/index.ts similarity index 90% rename from x-pack/plugins/infra/public/utils/formatters/index.ts rename to x-pack/plugins/infra/common/formatters/index.ts index 3c60dba747825..096085696bd6b 100644 --- a/x-pack/plugins/infra/public/utils/formatters/index.ts +++ b/x-pack/plugins/infra/common/formatters/index.ts @@ -5,12 +5,12 @@ */ import Mustache from 'mustache'; -import { InfraWaffleMapDataFormat } from '../../lib/lib'; import { createBytesFormatter } from './bytes'; import { formatNumber } from './number'; import { formatPercent } from './percent'; -import { InventoryFormatterType } from '../../../common/inventory_models/types'; +import { InventoryFormatterType } from '../inventory_models/types'; import { formatHighPercision } from './high_precision'; +import { InfraWaffleMapDataFormat } from './types'; export const FORMATTERS = { number: formatNumber, diff --git a/x-pack/plugins/infra/public/utils/formatters/number.ts b/x-pack/plugins/infra/common/formatters/number.ts similarity index 100% rename from x-pack/plugins/infra/public/utils/formatters/number.ts rename to x-pack/plugins/infra/common/formatters/number.ts diff --git a/x-pack/plugins/infra/public/utils/formatters/percent.ts b/x-pack/plugins/infra/common/formatters/percent.ts similarity index 100% rename from x-pack/plugins/infra/public/utils/formatters/percent.ts rename to x-pack/plugins/infra/common/formatters/percent.ts diff --git a/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts b/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts new file mode 100644 index 0000000000000..8b4ae27cb3061 --- /dev/null +++ b/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts @@ -0,0 +1,73 @@ +/* + * 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. + */ + +enum InfraFormatterType { + number = 'number', + abbreviatedNumber = 'abbreviatedNumber', + bytes = 'bytes', + bits = 'bits', + percent = 'percent', +} + +interface MetricFormatter { + formatter: InfraFormatterType; + template: string; + bounds?: { min: number; max: number }; +} + +interface MetricFormatters { + [key: string]: MetricFormatter; +} + +export const METRIC_FORMATTERS: MetricFormatters = { + ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' }, + ['cpu']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['memory']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['logRate']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}/s', + }, + ['diskIOReadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['diskIOWriteBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['s3BucketSize']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3TotalRequests']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3NumberOfObjects']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3UploadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3DownloadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['sqsOldestMessage']: { + formatter: InfraFormatterType.number, + template: '{{value}} seconds', + }, +}; diff --git a/x-pack/plugins/infra/common/formatters/types.ts b/x-pack/plugins/infra/common/formatters/types.ts new file mode 100644 index 0000000000000..c438ec2d4205d --- /dev/null +++ b/x-pack/plugins/infra/common/formatters/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum InfraWaffleMapDataFormat { + bytesDecimal = 'bytesDecimal', + bitsDecimal = 'bitsDecimal', + abbreviatedNumber = 'abbreviatedNumber', +} diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx index b2da7dec3f2e0..764db2164b711 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx @@ -11,27 +11,29 @@ import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_ import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const ec2MetricTypes: SnapshotMetricType[] = [ + 'cpu', + 'rx', + 'tx', + 'diskIOReadBytes', + 'diskIOWriteBytes', +]; + +export const ec2groupByFields = [ + 'cloud.availability_zone', + 'cloud.machine.type', + 'aws.ec2.instance.image.id', + 'aws.ec2.instance.state.name', +]; + export const AwsEC2ToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = [ - 'cpu', - 'rx', - 'tx', - 'diskIOReadBytes', - 'diskIOWriteBytes', - ]; - const groupByFields = [ - 'cloud.availability_zone', - 'cloud.machine.type', - 'aws.ec2.instance.image.id', - 'aws.ec2.instance.state.name', - ]; return ( <> ); diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx index 2a8394b9dd3a4..3eebdee22b2c3 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx @@ -11,26 +11,28 @@ import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_ import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const rdsMetricTypes: SnapshotMetricType[] = [ + 'cpu', + 'rdsConnections', + 'rdsQueriesExecuted', + 'rdsActiveTransactions', + 'rdsLatency', +]; + +export const rdsGroupByFields = [ + 'cloud.availability_zone', + 'aws.rds.db_instance.class', + 'aws.rds.db_instance.status', +]; + export const AwsRDSToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = [ - 'cpu', - 'rdsConnections', - 'rdsQueriesExecuted', - 'rdsActiveTransactions', - 'rdsLatency', - ]; - const groupByFields = [ - 'cloud.availability_zone', - 'aws.rds.db_instance.class', - 'aws.rds.db_instance.status', - ]; return ( <> ); diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx index 324bdd0586029..ede618b1bf19d 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx @@ -11,22 +11,24 @@ import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_ import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const s3MetricTypes: SnapshotMetricType[] = [ + 's3BucketSize', + 's3NumberOfObjects', + 's3TotalRequests', + 's3DownloadBytes', + 's3UploadBytes', +]; + +export const s3GroupByFields = ['cloud.region']; + export const AwsS3ToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = [ - 's3BucketSize', - 's3NumberOfObjects', - 's3TotalRequests', - 's3DownloadBytes', - 's3UploadBytes', - ]; - const groupByFields = ['cloud.region']; return ( <> ); diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx index 3229c07034772..e77f3af578197 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx @@ -11,22 +11,23 @@ import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_ import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const sqsMetricTypes: SnapshotMetricType[] = [ + 'sqsMessagesVisible', + 'sqsMessagesDelayed', + 'sqsMessagesSent', + 'sqsMessagesEmpty', + 'sqsOldestMessage', +]; +export const sqsGroupByFields = ['cloud.region']; + export const AwsSQSToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = [ - 'sqsMessagesVisible', - 'sqsMessagesDelayed', - 'sqsMessagesSent', - 'sqsMessagesEmpty', - 'sqsOldestMessage', - ]; - const groupByFields = ['cloud.region']; return ( <> ); diff --git a/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx index f6c707726d9ca..f193adbf6aadc 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx @@ -10,21 +10,22 @@ import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/compo import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const containerMetricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx']; +export const containerGroupByFields = [ + 'host.name', + 'cloud.availability_zone', + 'cloud.machine.type', + 'cloud.project.id', + 'cloud.provider', + 'service.type', +]; + export const ContainerToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx']; - const groupByFields = [ - 'host.name', - 'cloud.availability_zone', - 'cloud.machine.type', - 'cloud.project.id', - 'cloud.provider', - 'service.type', - ]; return ( ); }; diff --git a/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx index 136264c0e26f4..8ed684b3885de 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx @@ -10,20 +10,27 @@ import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/compo import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const hostMetricTypes: SnapshotMetricType[] = [ + 'cpu', + 'memory', + 'load', + 'rx', + 'tx', + 'logRate', +]; +export const hostGroupByFields = [ + 'cloud.availability_zone', + 'cloud.machine.type', + 'cloud.project.id', + 'cloud.provider', + 'service.type', +]; export const HostToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'load', 'rx', 'tx', 'logRate']; - const groupByFields = [ - 'cloud.availability_zone', - 'cloud.machine.type', - 'cloud.project.id', - 'cloud.provider', - 'service.type', - ]; return ( ); }; diff --git a/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx index c1cd375ff47bf..54a32e3e0180a 100644 --- a/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx @@ -10,14 +10,15 @@ import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/compo import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const podMetricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx']; +export const podGroupByFields = ['kubernetes.namespace', 'kubernetes.node.name', 'service.type']; + export const PodToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx']; - const groupByFields = ['kubernetes.namespace', 'kubernetes.node.name', 'service.type']; return ( ); }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx index bb664f4067662..8bcf0e9ed5be5 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AlertFlyout } from './alert_flyout'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -export const AlertDropdown = () => { +export const MetricsAlertDropdown = () => { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); const kibana = useKibana(); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index 352ac1927479e..5e14babddcb07 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -34,6 +34,8 @@ import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by'; import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; + import { ExpressionRow } from './expression_row'; import { AlertContextMeta, TimeUnit, MetricExpression } from '../types'; import { ExpressionChart } from './expression_chart'; @@ -45,6 +47,7 @@ interface Props { groupBy?: string; filterQuery?: string; sourceId?: string; + filterQueryText?: string; alertOnNoData?: boolean; }; alertsContext: AlertsContextValue; @@ -111,11 +114,15 @@ export const Expressions: React.FC = props => { [setAlertParams, alertParams.criteria] ); - const onFilterQuerySubmit = useCallback( + const onFilterChange = useCallback( (filter: any) => { - setAlertParams('filterQuery', filter); + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); }, - [setAlertParams] + [setAlertParams, derivedIndexPattern] ); const onGroupByChange = useCallback( @@ -180,10 +187,19 @@ export const Expressions: React.FC = props => { if (md.currentOptions) { if (md.currentOptions.filterQuery) { - setAlertParams('filterQuery', md.currentOptions.filterQuery); + setAlertParams('filterQueryText', md.currentOptions.filterQuery); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(md.currentOptions.filterQuery, derivedIndexPattern) || + '' + ); } else if (md.currentOptions.groupBy && md.series) { const filter = `${md.currentOptions.groupBy}: "${md.series.id}"`; - setAlertParams('filterQuery', filter); + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); } setAlertParams('groupBy', md.currentOptions.groupBy); @@ -200,8 +216,8 @@ export const Expressions: React.FC = props => { }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps const handleFieldSearchChange = useCallback( - (e: ChangeEvent) => onFilterQuerySubmit(e.target.value), - [onFilterQuerySubmit] + (e: ChangeEvent) => onFilterChange(e.target.value), + [onFilterChange] ); return ( @@ -304,13 +320,14 @@ export const Expressions: React.FC = props => { {(alertsContext.metadata && ( )) || ( )} diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx new file mode 100644 index 0000000000000..d2904206875c7 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx @@ -0,0 +1,62 @@ +/* + * 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, { useState, useCallback, useMemo } from 'react'; +import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertFlyout } from './alert_flyout'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; + +export const InventoryAlertDropdown = () => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [flyoutVisible, setFlyoutVisible] = useState(false); + const kibana = useKibana(); + + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, [setPopoverOpen]); + + const openPopover = useCallback(() => { + setPopoverOpen(true); + }, [setPopoverOpen]); + + const menuItems = useMemo(() => { + return [ + setFlyoutVisible(true)}> + + , + + + , + ]; + }, [kibana.services]); + + return ( + <> + + + + } + isOpen={popoverOpen} + closePopover={closePopover} + > + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx new file mode 100644 index 0000000000000..83298afd4fc5a --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.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, { useContext } from 'react'; +import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public'; +import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; +import { InfraWaffleMapOptions } from '../../../lib/lib'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; + +interface Props { + visible?: boolean; + options?: Partial; + nodeType?: InventoryItemType; + filter?: string; + setVisible: React.Dispatch>; +} + +export const AlertFlyout = (props: Props) => { + const { triggersActionsUI } = useContext(TriggerActionsContext); + const { services } = useKibana(); + + return ( + <> + {triggersActionsUI && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx new file mode 100644 index 0000000000000..15cad770836bd --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx @@ -0,0 +1,498 @@ +/* + * 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, { useCallback, useMemo, useEffect, useState, ChangeEvent } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, + EuiSpacer, + EuiText, + EuiFormRow, + EuiButtonEmpty, + EuiFieldSearch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + Comparator, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/metric_threshold/types'; +import { euiStyled } from '../../../../../observability/public'; +import { + ThresholdExpression, + ForLastExpression, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/common'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; +import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { toMetricOpt } from '../../../pages/metrics/inventory_view/components/toolbars/toolbar_wrapper'; +import { sqsMetricTypes } from '../../../../common/inventory_models/aws_sqs/toolbar_items'; +import { ec2MetricTypes } from '../../../../common/inventory_models/aws_ec2/toolbar_items'; +import { s3MetricTypes } from '../../../../common/inventory_models/aws_s3/toolbar_items'; +import { rdsMetricTypes } from '../../../../common/inventory_models/aws_rds/toolbar_items'; +import { hostMetricTypes } from '../../../../common/inventory_models/host/toolbar_items'; +import { containerMetricTypes } from '../../../../common/inventory_models/container/toolbar_items'; +import { podMetricTypes } from '../../../../common/inventory_models/pod/toolbar_items'; +import { findInventoryModel } from '../../../../common/inventory_models'; +import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; +import { MetricExpression } from './metric'; +import { NodeTypeExpression } from './node_type'; +import { InfraWaffleMapOptions } from '../../../lib/lib'; +import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; + +interface AlertContextMeta { + options?: Partial; + nodeType?: InventoryItemType; + filter?: string; +} + +interface Props { + errors: IErrorObject[]; + alertParams: { + criteria: InventoryMetricConditions[]; + nodeType: InventoryItemType; + groupBy?: string; + filterQuery?: string; + filterQueryText?: string; + sourceId?: string; + }; + alertsContext: AlertsContextValue; + setAlertParams(key: string, value: any): void; + setAlertProperty(key: string, value: any): void; +} + +type TimeUnit = 's' | 'm' | 'h' | 'd'; + +const defaultExpression = { + metric: 'cpu' as SnapshotMetricType, + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', +} as InventoryMetricConditions; + +export const Expressions: React.FC = props => { + const { setAlertParams, alertParams, errors, alertsContext } = props; + const { source, createDerivedIndexPattern } = useSourceViaHttp({ + sourceId: 'default', + type: 'metrics', + fetch: alertsContext.http.fetch, + toastWarning: alertsContext.toastNotifications.addWarning, + }); + const [timeSize, setTimeSize] = useState(1); + const [timeUnit, setTimeUnit] = useState('m'); + + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + createDerivedIndexPattern, + ]); + + const updateParams = useCallback( + (id, e: InventoryMetricConditions) => { + const exp = alertParams.criteria ? alertParams.criteria.slice() : []; + exp[id] = { ...exp[id], ...e }; + setAlertParams('criteria', exp); + }, + [setAlertParams, alertParams.criteria] + ); + + const addExpression = useCallback(() => { + const exp = alertParams.criteria.slice(); + exp.push(defaultExpression); + setAlertParams('criteria', exp); + }, [setAlertParams, alertParams.criteria]); + + const removeExpression = useCallback( + (id: number) => { + const exp = alertParams.criteria.slice(); + if (exp.length > 1) { + exp.splice(id, 1); + setAlertParams('criteria', exp); + } + }, + [setAlertParams, alertParams.criteria] + ); + + const onFilterChange = useCallback( + (filter: any) => { + setAlertParams('filterQueryText', filter || ''); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); + }, + [derivedIndexPattern, setAlertParams] + ); + + const emptyError = useMemo(() => { + return { + aggField: [], + timeSizeUnit: [], + timeWindowSize: [], + }; + }, []); + + const updateTimeSize = useCallback( + (ts: number | undefined) => { + const criteria = alertParams.criteria.map(c => ({ + ...c, + timeSize: ts, + })); + setTimeSize(ts || undefined); + setAlertParams('criteria', criteria); + }, + [alertParams.criteria, setAlertParams] + ); + + const updateTimeUnit = useCallback( + (tu: string) => { + const criteria = alertParams.criteria.map(c => ({ + ...c, + timeUnit: tu, + })); + setTimeUnit(tu as TimeUnit); + setAlertParams('criteria', criteria); + }, + [alertParams.criteria, setAlertParams] + ); + + const updateNodeType = useCallback( + (nt: any) => { + setAlertParams('nodeType', nt); + }, + [setAlertParams] + ); + + const handleFieldSearchChange = useCallback( + (e: ChangeEvent) => onFilterChange(e.target.value), + [onFilterChange] + ); + + useEffect(() => { + const md = alertsContext.metadata; + if (!alertParams.nodeType) { + if (md && md.nodeType) { + setAlertParams('nodeType', md.nodeType); + } else { + setAlertParams('nodeType', 'host'); + } + } + + if (!alertParams.criteria) { + if (md && md.options) { + setAlertParams('criteria', [ + { + ...defaultExpression, + metric: md.options.metric!.type, + } as InventoryMetricConditions, + ]); + } else { + setAlertParams('criteria', [defaultExpression]); + } + } + + if (!alertParams.filterQuery) { + if (md && md.filter) { + setAlertParams('filterQueryText', md.filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(md.filter, derivedIndexPattern) || '' + ); + } + } + + if (!alertParams.sourceId) { + setAlertParams('sourceId', source?.id); + } + }, [alertsContext.metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + <> + + +

    + +

    +
    + + + + + {alertParams.criteria && + alertParams.criteria.map((e, idx) => { + return ( + 1} + remove={removeExpression} + addExpression={addExpression} + key={idx} // idx's don't usually make good key's but here the index has semantic meaning + expressionId={idx} + setAlertParams={updateParams} + errors={errors[idx] || emptyError} + expression={e || {}} + /> + ); + })} + + + +
    + + + +
    + + + + + {(alertsContext.metadata && ( + + )) || ( + + )} + + + + + ); +}; + +interface ExpressionRowProps { + nodeType: InventoryItemType; + expressionId: number; + expression: Omit & { + metric?: SnapshotMetricType; + }; + errors: IErrorObject; + canDelete: boolean; + addExpression(): void; + remove(id: number): void; + setAlertParams(id: number, params: Partial): void; +} + +const StyledExpressionRow = euiStyled(EuiFlexGroup)` + display: flex; + flex-wrap: wrap; + margin: 0 -4px; +`; + +const StyledExpression = euiStyled.div` + padding: 0 4px; +`; + +export const ExpressionRow: React.FC = props => { + const { setAlertParams, expression, errors, expressionId, remove, canDelete } = props; + const { metric, comparator = Comparator.GT, threshold = [] } = expression; + + const updateMetric = useCallback( + (m?: SnapshotMetricType) => { + setAlertParams(expressionId, { ...expression, metric: m }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateComparator = useCallback( + (c?: string) => { + setAlertParams(expressionId, { ...expression, comparator: c as Comparator | undefined }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateThreshold = useCallback( + t => { + if (t.join() !== expression.threshold.join()) { + setAlertParams(expressionId, { ...expression, threshold: t }); + } + }, + [expressionId, expression, setAlertParams] + ); + + const ofFields = useMemo(() => { + let myMetrics = hostMetricTypes; + + switch (props.nodeType) { + case 'awsEC2': + myMetrics = ec2MetricTypes; + break; + case 'awsRDS': + myMetrics = rdsMetricTypes; + break; + case 'awsS3': + myMetrics = s3MetricTypes; + break; + case 'awsSQS': + myMetrics = sqsMetricTypes; + break; + case 'host': + myMetrics = hostMetricTypes; + break; + case 'pod': + myMetrics = podMetricTypes; + break; + case 'container': + myMetrics = containerMetricTypes; + break; + } + return myMetrics.map(toMetricOpt); + }, [props.nodeType]); + + return ( + <> + + + + + v?.value === metric)?.text || '', + }} + metrics={ + ofFields.filter(m => m !== undefined && m.value !== undefined) as Array<{ + value: SnapshotMetricType; + text: string; + }> + } + onChange={updateMetric} + errors={errors} + /> + + + + + {metric && ( + +
    +
    {metricUnit[metric]?.label || ''}
    +
    +
    + )} +
    +
    + {canDelete && ( + + remove(expressionId)} + /> + + )} +
    + + + ); +}; + +const getDisplayNameForType = (type: InventoryItemType) => { + const inventoryModel = findInventoryModel(type); + return inventoryModel.displayName; +}; + +export const nodeTypes: { [key: string]: any } = { + host: { + text: getDisplayNameForType('host'), + value: 'host', + }, + pod: { + text: getDisplayNameForType('pod'), + value: 'pod', + }, + container: { + text: getDisplayNameForType('container'), + value: 'container', + }, + awsEC2: { + text: getDisplayNameForType('awsEC2'), + value: 'awsEC2', + }, + awsS3: { + text: getDisplayNameForType('awsS3'), + value: 'awsS3', + }, + awsRDS: { + text: getDisplayNameForType('awsRDS'), + value: 'awsRDS', + }, + awsSQS: { + text: getDisplayNameForType('awsSQS'), + value: 'awsSQS', + }, +}; + +const metricUnit: Record = { + count: { label: '' }, + cpu: { label: '%' }, + memory: { label: '%' }, + rx: { label: 'bits/s' }, + tx: { label: 'bits/s' }, + logRate: { label: '/s' }, + diskIOReadBytes: { label: 'bytes/s' }, + diskIOWriteBytes: { label: 'bytes/s' }, + s3BucketSize: { label: 'bytes' }, + s3TotalRequests: { label: '' }, + s3NumberOfObjects: { label: '' }, + s3UploadBytes: { label: 'bytes' }, + s3DownloadBytes: { label: 'bytes' }, + sqsOldestMessage: { label: 'seconds' }, +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx new file mode 100644 index 0000000000000..faafdf1b81eed --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx @@ -0,0 +1,150 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiExpression, + EuiPopover, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiComboBox, +} from '@elastic/eui'; +import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +import { SnapshotMetricType } from '../../../../common/inventory_models/types'; + +interface Props { + metric?: { value: SnapshotMetricType; text: string }; + metrics: Array<{ value: string; text: string }>; + errors: IErrorObject; + onChange: (metric: SnapshotMetricType) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosition }: Props) => { + const [aggFieldPopoverOpen, setAggFieldPopoverOpen] = useState(false); + const firstFieldOption = { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.metric.selectFieldLabel', { + defaultMessage: 'Select a metric', + }), + value: '', + }; + + const availablefieldsOptions = metrics.map(m => { + return { label: m.text, value: m.value }; + }, []); + + return ( + { + setAggFieldPopoverOpen(true); + }} + color={metric ? 'secondary' : 'danger'} + /> + } + isOpen={aggFieldPopoverOpen} + closePopover={() => { + setAggFieldPopoverOpen(false); + }} + withTitle + anchorPosition={popupPosition ?? 'downRight'} + zIndex={8000} + > +
    + setAggFieldPopoverOpen(false)}> + + + + + 0 && metric !== undefined} + error={errors.metric} + > + 0 && metric !== undefined} + placeholder={firstFieldOption.text} + options={availablefieldsOptions} + noSuggestions={!availablefieldsOptions.length} + selectedOptions={ + metric ? availablefieldsOptions.filter(a => a.value === metric.value) : [] + } + renderOption={(o: any) => o.label} + onChange={selectedOptions => { + if (selectedOptions.length > 0) { + onChange(selectedOptions[0].value as SnapshotMetricType); + setAggFieldPopoverOpen(false); + } + }} + /> + + + +
    +
    + ); +}; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { + return ( + + + {children} + + onClose()} + /> + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts b/x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts new file mode 100644 index 0000000000000..b7abaf5b36373 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts @@ -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 { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types'; +import { Expressions } from './expression'; +import { validateMetricThreshold } from './validation'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; + +export function getInventoryMetricAlertType(): AlertTypeModel { + return { + id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + name: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertName', { + defaultMessage: 'Inventory', + }), + iconClass: 'bell', + alertParamsExpression: Expressions, + validate: validateMetricThreshold, + defaultActionMessage: i18n.translate( + 'xpack.infra.metrics.alerting.inventory.threshold.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} - \\{\\{context.group\\}\\} + +\\{\\{context.metricOf.condition0\\}\\} has crossed a threshold of \\{\\{context.thresholdOf.condition0\\}\\} +Current value is \\{\\{context.valueOf.condition0\\}\\} +`, + } + ), + }; +} diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx new file mode 100644 index 0000000000000..1623fc4e24dcb --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx @@ -0,0 +1,115 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; + +interface WhenExpressionProps { + value: InventoryItemType; + options: { [key: string]: { text: string; value: InventoryItemType } }; + onChange: (value: InventoryItemType) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const NodeTypeExpression = ({ + value, + options, + onChange, + popupPosition, +}: WhenExpressionProps) => { + const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); + + return ( + { + setAggTypePopoverOpen(true); + }} + /> + } + isOpen={aggTypePopoverOpen} + closePopover={() => { + setAggTypePopoverOpen(false); + }} + ownFocus + withTitle + anchorPosition={popupPosition ?? 'downLeft'} + > +
    + setAggTypePopoverOpen(false)}> + + + { + onChange(e.target.value as InventoryItemType); + setAggTypePopoverOpen(false); + }} + options={Object.values(options).map(o => o)} + /> +
    +
    + ); +}; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { + return ( + + + {children} + + onClose()} + /> + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx new file mode 100644 index 0000000000000..803893dd5a323 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx @@ -0,0 +1,80 @@ +/* + * 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'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MetricExpressionParams } from '../../../../server/lib/alerting/metric_threshold/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; + +export function validateMetricThreshold({ + criteria, +}: { + criteria: MetricExpressionParams[]; +}): ValidationResult { + const validationResult = { errors: {} }; + const errors: { + [id: string]: { + timeSizeUnit: string[]; + timeWindowSize: string[]; + threshold0: string[]; + threshold1: string[]; + metric: string[]; + }; + } = {}; + validationResult.errors = errors; + + if (!criteria || !criteria.length) { + return validationResult; + } + + criteria.forEach((c, idx) => { + // Create an id for each criteria, so we can map errors to specific criteria. + const id = idx.toString(); + + errors[id] = errors[id] || { + timeSizeUnit: [], + timeWindowSize: [], + threshold0: [], + threshold1: [], + metric: [], + }; + + if (!c.threshold || !c.threshold.length) { + errors[id].threshold0.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { + defaultMessage: 'Threshold is required.', + }) + ); + } + + if (c.comparator === 'between' && (!c.threshold || c.threshold.length < 2)) { + errors[id].threshold1.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { + defaultMessage: 'Threshold is required.', + }) + ); + } + + if (!c.timeSize) { + errors[id].timeWindowSize.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.timeRequred', { + defaultMessage: 'Time size is Required.', + }) + ); + } + + if (!c.metric && c.aggType !== 'count') { + errors[id].metric.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.metricRequired', { + defaultMessage: 'Metric is required.', + }) + ); + } + }); + + return validationResult; +} diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx index 72d6aea5ecfc6..c713839a1bba8 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx @@ -22,7 +22,7 @@ import { } from './log_entry_column'; import { ASSUMED_SCROLLBAR_WIDTH } from './vertical_scroll_panel'; import { LogPositionState } from '../../../containers/logs/log_position'; -import { localizedDate } from '../../../utils/formatters/datetime'; +import { localizedDate } from '../../../../common/formatters/datetime'; export const LogColumnHeaders: React.FunctionComponent<{ columnConfigurations: LogColumnConfiguration[]; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx index fbc450950b828..144caed744bab 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiTitle } from '@elastic/eui'; -import { localizedDate } from '../../../utils/formatters/datetime'; +import { localizedDate } from '../../../../common/formatters/datetime'; interface LogDateRowProps { timestamp: number; diff --git a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts index bc6374a6538e3..aad54bd2222b7 100644 --- a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts +++ b/x-pack/plugins/infra/public/containers/source/use_source_via_http.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 { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useCallback } from 'react'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -69,12 +69,15 @@ export const useSourceViaHttp = ({ })(); }, [makeRequest]); - const createDerivedIndexPattern = (indexType: 'logs' | 'metrics' | 'both' = type) => { - return { - fields: response?.source ? response.status.indexFields : [], - title: pickIndexPattern(response?.source, indexType), - }; - }; + const createDerivedIndexPattern = useCallback( + (indexType: 'logs' | 'metrics' | 'both' = type) => { + return { + fields: response?.source ? response.status.indexFields : [], + title: pickIndexPattern(response?.source, indexType), + }; + }, + [response, type] + ); const source = useMemo(() => { return response ? { ...response.source, status: response.status } : null; diff --git a/x-pack/plugins/infra/public/index.ts b/x-pack/plugins/infra/public/index.ts index 4465bde377c12..1dfdf827f203b 100644 --- a/x-pack/plugins/infra/public/index.ts +++ b/x-pack/plugins/infra/public/index.ts @@ -16,7 +16,7 @@ export const plugin: PluginInitializer< return new Plugin(context); }; -export { FORMATTERS } from './utils/formatters'; +export { FORMATTERS } from '../common/formatters'; export { InfraFormatterType } from './lib/lib'; export type InfraAppId = 'logs' | 'metrics'; diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts index e4de0caf9bb8b..9043b4d9f6979 100644 --- a/x-pack/plugins/infra/public/lib/lib.ts +++ b/x-pack/plugins/infra/public/lib/lib.ts @@ -186,12 +186,6 @@ export enum InfraFormatterType { percent = 'percent', } -export enum InfraWaffleMapDataFormat { - bytesDecimal = 'bytesDecimal', - bitsDecimal = 'bitsDecimal', - abbreviatedNumber = 'abbreviatedNumber', -} - export interface InfraGroupByOptions { text: string; field: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 5dc9802fefd25..dbf71665ea869 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -28,7 +28,9 @@ import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options'; import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time'; import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters'; -import { AlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; + +import { InventoryAlertDropdown } from '../../components/alerting/inventory/alert_dropdown'; +import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; export const InfrastructurePage = ({ match }: RouteComponentProps) => { const uiCapabilities = useKibana().services.application?.capabilities; @@ -96,7 +98,8 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { /> - + + diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx index c528aa885346e..d576f08108649 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo, useState } from 'react'; -import { AlertFlyout } from '../../../../../alerting/metric_threshold/components/alert_flyout'; +import { AlertFlyout } from '../../../../../components/alerting/inventory/alert_flyout'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; import { getNodeDetailUrl, getNodeLogsUrl } from '../../../../link_to'; import { createUptimeLink } from '../../lib/create_uptime_link'; @@ -24,6 +24,8 @@ import { SectionSubtitle, SectionLinks, SectionLink, + withTheme, + EuiTheme, } from '../../../../../../../observability/public'; import { useLinkProps } from '../../../../../hooks/use_link_props'; @@ -37,157 +39,178 @@ interface Props { popoverPosition: EuiPopoverProps['anchorPosition']; } -export const NodeContextMenu: React.FC = ({ - options, - currentTime, - children, - node, - isPopoverOpen, - closePopover, - nodeType, - popoverPosition, -}) => { - const [flyoutVisible, setFlyoutVisible] = useState(false); - const inventoryModel = findInventoryModel(nodeType); - const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; - const uiCapabilities = useKibana().services.application?.capabilities; - // Due to the changing nature of the fields between APM and this UI, - // We need to have some exceptions until 7.0 & ECS is finalized. Reference - // #26620 for the details for these fields. - // TODO: This is tech debt, remove it after 7.0 & ECS migration. - const apmField = nodeType === 'host' ? 'host.hostname' : inventoryModel.fields.id; +export const NodeContextMenu: React.FC = withTheme( + ({ + options, + currentTime, + children, + node, + isPopoverOpen, + closePopover, + nodeType, + popoverPosition, + theme, + }) => { + const [flyoutVisible, setFlyoutVisible] = useState(false); + const inventoryModel = findInventoryModel(nodeType); + const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; + const uiCapabilities = useKibana().services.application?.capabilities; + // Due to the changing nature of the fields between APM and this UI, + // We need to have some exceptions until 7.0 & ECS is finalized. Reference + // #26620 for the details for these fields. + // TODO: This is tech debt, remove it after 7.0 & ECS migration. + const apmField = nodeType === 'host' ? 'host.hostname' : inventoryModel.fields.id; - const showDetail = inventoryModel.crosslinkSupport.details; - const showLogsLink = - inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities?.logs?.show; - const showAPMTraceLink = - inventoryModel.crosslinkSupport.apm && uiCapabilities?.apm && uiCapabilities?.apm.show; - const showUptimeLink = - inventoryModel.crosslinkSupport.uptime && (['pod', 'container'].includes(nodeType) || node.ip); + const showDetail = inventoryModel.crosslinkSupport.details; + const showLogsLink = + inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities?.logs?.show; + const showAPMTraceLink = + inventoryModel.crosslinkSupport.apm && uiCapabilities?.apm && uiCapabilities?.apm.show; + const showUptimeLink = + inventoryModel.crosslinkSupport.uptime && + (['pod', 'container'].includes(nodeType) || node.ip); - const inventoryId = useMemo(() => { - if (nodeType === 'host') { - if (node.ip) { - return { label: host.ip, value: node.ip }; + const inventoryId = useMemo(() => { + if (nodeType === 'host') { + if (node.ip) { + return { label: host.ip, value: node.ip }; + } + } else { + if (options.fields) { + const { id } = findInventoryFields(nodeType, options.fields); + return { + label: {id}, + value: node.id, + }; + } } - } else { - if (options.fields) { - const { id } = findInventoryFields(nodeType, options.fields); - return { - label: {id}, - value: node.id, - }; - } - } - return { label: '', value: '' }; - }, [nodeType, node.ip, node.id, options.fields]); + return { label: '', value: '' }; + }, [nodeType, node.ip, node.id, options.fields]); + + const nodeLogsMenuItemLinkProps = useLinkProps({ + app: 'logs', + ...getNodeLogsUrl({ + nodeType, + nodeId: node.id, + time: currentTime, + }), + }); + const nodeDetailMenuItemLinkProps = useLinkProps({ + ...getNodeDetailUrl({ + nodeType, + nodeId: node.id, + from: nodeDetailFrom, + to: currentTime, + }), + }); + const apmTracesMenuItemLinkProps = useLinkProps({ + app: 'apm', + hash: 'traces', + search: { + kuery: `${apmField}:"${node.id}"`, + }, + }); + const uptimeMenuItemLinkProps = useLinkProps(createUptimeLink(options, nodeType, node)); - const nodeLogsMenuItemLinkProps = useLinkProps({ - app: 'logs', - ...getNodeLogsUrl({ - nodeType, - nodeId: node.id, - time: currentTime, - }), - }); - const nodeDetailMenuItemLinkProps = useLinkProps({ - ...getNodeDetailUrl({ - nodeType, - nodeId: node.id, - from: nodeDetailFrom, - to: currentTime, - }), - }); - const apmTracesMenuItemLinkProps = useLinkProps({ - app: 'apm', - hash: 'traces', - search: { - kuery: `${apmField}:"${node.id}"`, - }, - }); - const uptimeMenuItemLinkProps = useLinkProps(createUptimeLink(options, nodeType, node)); + const nodeLogsMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', { + defaultMessage: '{inventoryName} logs', + values: { inventoryName: inventoryModel.singularDisplayName }, + }), + ...nodeLogsMenuItemLinkProps, + 'data-test-subj': 'viewLogsContextMenuItem', + isDisabled: !showLogsLink, + }; - const nodeLogsMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', { - defaultMessage: '{inventoryName} logs', - values: { inventoryName: inventoryModel.singularDisplayName }, - }), - ...nodeLogsMenuItemLinkProps, - 'data-test-subj': 'viewLogsContextMenuItem', - isDisabled: !showLogsLink, - }; + const nodeDetailMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', { + defaultMessage: '{inventoryName} metrics', + values: { inventoryName: inventoryModel.singularDisplayName }, + }), + ...nodeDetailMenuItemLinkProps, + isDisabled: !showDetail, + }; - const nodeDetailMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', { - defaultMessage: '{inventoryName} metrics', - values: { inventoryName: inventoryModel.singularDisplayName }, - }), - ...nodeDetailMenuItemLinkProps, - isDisabled: !showDetail, - }; + const apmTracesMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', { + defaultMessage: '{inventoryName} APM traces', + values: { inventoryName: inventoryModel.singularDisplayName }, + }), + ...apmTracesMenuItemLinkProps, + 'data-test-subj': 'viewApmTracesContextMenuItem', + isDisabled: !showAPMTraceLink, + }; - const apmTracesMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', { - defaultMessage: '{inventoryName} APM traces', - values: { inventoryName: inventoryModel.singularDisplayName }, - }), - ...apmTracesMenuItemLinkProps, - 'data-test-subj': 'viewApmTracesContextMenuItem', - isDisabled: !showAPMTraceLink, - }; + const uptimeMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', { + defaultMessage: '{inventoryName} in Uptime', + values: { inventoryName: inventoryModel.singularDisplayName }, + }), + ...uptimeMenuItemLinkProps, + isDisabled: !showUptimeLink, + }; - const uptimeMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', { - defaultMessage: '{inventoryName} in Uptime', - values: { inventoryName: inventoryModel.singularDisplayName }, - }), - ...uptimeMenuItemLinkProps, - isDisabled: !showUptimeLink, - }; + const createAlertMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.createAlertLink', { + defaultMessage: 'Create alert', + }), + style: { color: theme?.eui.euiLinkColor || '#006BB4', fontWeight: 500, padding: 0 }, + onClick: () => { + setFlyoutVisible(true); + }, + }; - return ( - <> - -
    -
    - - - - {inventoryId.label && ( - -
    - -
    -
    - )} - - - - - - -
    -
    -
    - - - ); -}; + return ( + <> + +
    +
    + + + + {inventoryId.label && ( + +
    + +
    +
    + )} + + + + + + + +
    +
    +
    + + + ); + } +); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts index acd71e5137694..f8c7a10f12831 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts @@ -5,13 +5,13 @@ */ import { get } from 'lodash'; -import { createFormatter } from '../../../../utils/formatters'; import { InfraFormatterType } from '../../../../lib/lib'; import { SnapshotMetricInput, SnapshotCustomMetricInputRT, } from '../../../../../common/http_api/snapshot_api'; import { createFormatterForMetric } from '../../metrics_explorer/components/helpers/create_formatter_for_metric'; +import { createFormatter } from '../../../../../common/formatters'; interface MetricFormatter { formatter: InfraFormatterType; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx index 0aab676b7d6c5..0f53ced80888b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx @@ -17,7 +17,7 @@ import { get, last, max } from 'lodash'; import React, { ReactText } from 'react'; import { euiStyled } from '../../../../../../observability/public'; -import { createFormatter } from '../../../../utils/formatters'; +import { createFormatter } from '../../../../../common/formatters'; import { InventoryFormatterType } from '../../../../../common/inventory_models/types'; import { SeriesOverrides, VisSectionProps } from '../types'; import { getChartName } from './helpers'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts index bb4ad32660952..0b8773db2dddf 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts @@ -7,7 +7,7 @@ import { ReactText } from 'react'; import Color from 'color'; import { get, first, last, min, max } from 'lodash'; -import { createFormatter } from '../../../../utils/formatters'; +import { createFormatter } from '../../../../../common/formatters'; import { InfraDataSeries } from '../../../../graphql/types'; import { InventoryVisTypeRT, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts index d07a6b45f02be..46bd7b006446a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts @@ -5,7 +5,7 @@ */ import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; -import { createFormatter } from '../../../../../utils/formatters'; +import { createFormatter } from '../../../../../../common/formatters'; import { InfraFormatterType } from '../../../../../lib/lib'; import { metricToFormat } from './metric_to_format'; export const createFormatterForMetric = (metric?: MetricsExplorerMetric) => { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx index e9826e1ff3955..04661bbc37702 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx @@ -14,6 +14,7 @@ import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data interface Props { derivedIndexPattern: IIndexPattern; onSubmit: (query: string) => void; + onChange?: (query: string) => void; value?: string | null; placeholder?: string; } @@ -30,6 +31,7 @@ function validateQuery(query: string) { export const MetricsExplorerKueryBar = ({ derivedIndexPattern, onSubmit, + onChange, value, placeholder, }: Props) => { @@ -46,6 +48,9 @@ export const MetricsExplorerKueryBar = ({ const handleChange = (query: string) => { setValidation(validateQuery(query)); setDraftQuery(query); + if (onChange) { + onChange(query); + } }; const filteredDerivedIndexPattern = { diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 8cdfc4f381f43..d61ef7fc4a631 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -22,6 +22,7 @@ import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type'; +import { getInventoryMetricAlertType } from './components/alerting/inventory/metric_inventory_threshold_alert_type'; import { createMetricThresholdAlertType } from './alerting/metric_threshold'; export type ClientSetup = void; @@ -53,6 +54,7 @@ export class Plugin setup(core: CoreSetup, pluginsSetup: ClientPluginsSetup) { registerFeatures(pluginsSetup.home); + pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getInventoryMetricAlertType()); pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getLogsAlertType()); pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(createMetricThresholdAlertType()); diff --git a/x-pack/plugins/infra/server/graphql/sources/resolvers.ts b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts index f880eca933241..cffab4ba4f6f0 100644 --- a/x-pack/plugins/infra/server/graphql/sources/resolvers.ts +++ b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts @@ -101,7 +101,9 @@ export const createSourcesResolvers = ( return requestedSourceConfiguration; }, async allSources(root, args, { req }) { - const sourceConfigurations = await libs.sources.getAllSourceConfigurations(req); + const sourceConfigurations = await libs.sources.getAllSourceConfigurations( + req.core.savedObjects.client + ); return sourceConfigurations; }, @@ -131,7 +133,7 @@ export const createSourcesResolvers = ( Mutation: { async createSource(root, args, { req }) { const sourceConfiguration = await libs.sources.createSourceConfiguration( - req, + req.core.savedObjects.client, args.id, compactObject({ ...args.sourceProperties, @@ -147,7 +149,7 @@ export const createSourcesResolvers = ( }; }, async deleteSource(root, args, { req }) { - await libs.sources.deleteSourceConfiguration(req, args.id); + await libs.sources.deleteSourceConfiguration(req.core.savedObjects.client, args.id); return { id: args.id, @@ -155,7 +157,7 @@ export const createSourcesResolvers = ( }, async updateSource(root, args, { req }) { const updatedSourceConfiguration = await libs.sources.updateSourceConfiguration( - req, + req.core.savedObjects.client, args.id, compactObject({ ...args.sourceProperties, diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index 5a5f9d0f8f529..62f324e01f8d9 100644 --- a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -18,6 +18,7 @@ import { InventoryMetricRT, } from '../../../../common/inventory_models/types'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; +import { CallWithRequestParams, InfraDatabaseSearchResponse } from '../framework'; export class KibanaMetricsAdapter implements InfraMetricsAdapter { private framework: KibanaFramework; @@ -120,9 +121,14 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { indexPattern, options.timerange.interval ); + + const client = ( + opts: CallWithRequestParams + ): Promise> => + this.framework.callWithRequest(requestContext, 'search', opts); + const calculatedInterval = await calculateMetricInterval( - this.framework, - requestContext, + client, { indexPattern: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`, timestampField: options.sourceConfiguration.fields.timestamp, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts new file mode 100644 index 0000000000000..cc8a35f6e47a1 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -0,0 +1,214 @@ +/* + * 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 { mapValues, last, get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { + InfraDatabaseSearchResponse, + CallWithRequestParams, +} from '../../adapters/framework/adapter_types'; +import { Comparator, AlertStates, InventoryMetricConditions } from './types'; +import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; +import { InfraSnapshot } from '../../snapshot'; +import { parseFilterQuery } from '../../../utils/serialized_query'; +import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { InfraTimerangeInput } from '../../../../common/http_api/snapshot_api'; +import { InfraSourceConfiguration } from '../../sources'; +import { InfraBackendLibs } from '../../infra_types'; +import { METRIC_FORMATTERS } from '../../../../common/formatters/snapshot_metric_formats'; +import { createFormatter } from '../../../../common/formatters'; + +interface InventoryMetricThresholdParams { + criteria: InventoryMetricConditions[]; + groupBy: string | undefined; + filterQuery: string | undefined; + nodeType: InventoryItemType; + sourceId?: string; +} + +export const createInventoryMetricThresholdExecutor = ( + libs: InfraBackendLibs, + alertId: string +) => async ({ services, params }: AlertExecutorOptions) => { + const { criteria, filterQuery, sourceId, nodeType } = params as InventoryMetricThresholdParams; + + const source = await libs.sources.getSourceConfiguration( + services.savedObjectsClient, + sourceId || 'default' + ); + + const results = await Promise.all( + criteria.map(c => evaluateCondtion(c, nodeType, source.configuration, services, filterQuery)) + ); + + const invenotryItems = Object.keys(results[0]); + for (const item of invenotryItems) { + const alertInstance = services.alertInstanceFactory(`${alertId}-${item}`); + // AND logic; all criteria must be across the threshold + const shouldAlertFire = results.every(result => result[item].shouldFire); + + // AND logic; because we need to evaluate all criteria, if one of them reports no data then the + // whole alert is in a No Data/Error state + const isNoData = results.some(result => result[item].isNoData); + const isError = results.some(result => result[item].isError); + + if (shouldAlertFire) { + alertInstance.scheduleActions(FIRED_ACTIONS.id, { + group: item, + item, + valueOf: mapToConditionsLookup(results, result => + formatMetric(result[item].metric, result[item].currentValue) + ), + thresholdOf: mapToConditionsLookup(criteria, c => c.threshold), + metricOf: mapToConditionsLookup(criteria, c => c.metric), + }); + } + + alertInstance.replaceState({ + alertState: isError + ? AlertStates.ERROR + : isNoData + ? AlertStates.NO_DATA + : shouldAlertFire + ? AlertStates.ALERT + : AlertStates.OK, + }); + } +}; + +interface ConditionResult { + shouldFire: boolean; + currentValue?: number | null; + isNoData: boolean; + isError: boolean; +} + +const evaluateCondtion = async ( + condition: InventoryMetricConditions, + nodeType: InventoryItemType, + sourceConfiguration: InfraSourceConfiguration, + services: AlertServices, + filterQuery?: string +): Promise> => { + const { comparator, metric } = condition; + let { threshold } = condition; + + const currentValues = await getData( + services, + nodeType, + metric, + { + to: Date.now(), + from: moment() + .subtract(condition.timeSize, condition.timeUnit) + .toDate() + .getTime(), + interval: condition.timeUnit, + }, + sourceConfiguration, + filterQuery + ); + + threshold = threshold.map(n => convertMetricValue(metric, n)); + + const comparisonFunction = comparatorMap[comparator]; + + return mapValues(currentValues, value => ({ + shouldFire: value !== undefined && value !== null && comparisonFunction(value, threshold), + metric, + currentValue: value, + isNoData: value === null, + isError: value === undefined, + })); +}; + +const getData = async ( + services: AlertServices, + nodeType: InventoryItemType, + metric: SnapshotMetricType, + timerange: InfraTimerangeInput, + sourceConfiguration: InfraSourceConfiguration, + filterQuery?: string +) => { + const snapshot = new InfraSnapshot(); + const esClient = ( + options: CallWithRequestParams + ): Promise> => + services.callCluster('search', options); + + const options = { + filterQuery: parseFilterQuery(filterQuery), + nodeType, + groupBy: [], + sourceConfiguration, + metric: { type: metric }, + timerange, + }; + + const { nodes } = await snapshot.getNodes(esClient, options); + + return nodes.reduce((acc, n) => { + const nodePathItem = last(n.path); + acc[nodePathItem.label] = n.metric && n.metric.value; + return acc; + }, {} as Record); +}; + +const comparatorMap = { + [Comparator.BETWEEN]: (value: number, [a, b]: number[]) => + value >= Math.min(a, b) && value <= Math.max(a, b), + // `threshold` is always an array of numbers in case the BETWEEN comparator is + // used; all other compartors will just destructure the first value in the array + [Comparator.GT]: (a: number, [b]: number[]) => a > b, + [Comparator.LT]: (a: number, [b]: number[]) => a < b, + [Comparator.OUTSIDE_RANGE]: (value: number, [a, b]: number[]) => value < a || value > b, + [Comparator.GT_OR_EQ]: (a: number, [b]: number[]) => a >= b, + [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b, +}; + +const mapToConditionsLookup = ( + list: any[], + mapFn: (value: any, index: number, array: any[]) => unknown +) => + list + .map(mapFn) + .reduce( + (result: Record, value, i) => ({ ...result, [`condition${i}`]: value }), + {} + ); + +export const FIRED_ACTIONS = { + id: 'metrics.invenotry_threshold.fired', + name: i18n.translate('xpack.infra.metrics.alerting.inventory.threshold.fired', { + defaultMessage: 'Fired', + }), +}; + +// Some metrics in the UI are in a different unit that what we store in ES. +const convertMetricValue = (metric: SnapshotMetricType, value: number) => { + if (converters[metric]) { + return converters[metric](value); + } else { + return value; + } +}; +const converters: Record number> = { + cpu: n => Number(n) / 100, + memory: n => Number(n) / 100, +}; + +const formatMetric = (metric: SnapshotMetricType, value: number) => { + // if (SnapshotCustomMetricInputRT.is(metric)) { + // const formatter = createFormatterForMetric(metric); + // return formatter(val); + // } + const metricFormatter = get(METRIC_FORMATTERS, metric, METRIC_FORMATTERS.count); + if (value == null) { + return ''; + } + const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template); + return formatter(value); +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts new file mode 100644 index 0000000000000..3b6a1b5557bc6 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts @@ -0,0 +1,92 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { curry } from 'lodash'; +import uuid from 'uuid'; +import { + createInventoryMetricThresholdExecutor, + FIRED_ACTIONS, +} from './inventory_metric_threshold_executor'; +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './types'; +import { InfraBackendLibs } from '../../infra_types'; + +const condition = schema.object({ + threshold: schema.arrayOf(schema.number()), + comparator: schema.oneOf([ + schema.literal('>'), + schema.literal('<'), + schema.literal('>='), + schema.literal('<='), + schema.literal('between'), + schema.literal('outside'), + ]), + timeUnit: schema.string(), + timeSize: schema.number(), + metric: schema.string(), +}); + +export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs) => ({ + id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + name: 'Inventory', + validate: { + params: schema.object( + { + criteria: schema.arrayOf(condition), + nodeType: schema.string(), + filterQuery: schema.maybe(schema.string()), + sourceId: schema.string(), + }, + { unknowns: 'allow' } + ), + }, + defaultActionGroupId: FIRED_ACTIONS.id, + actionGroups: [FIRED_ACTIONS], + executor: curry(createInventoryMetricThresholdExecutor)(libs, uuid.v4()), + actionVariables: { + context: [ + { + name: 'group', + description: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.groupActionVariableDescription', + { + defaultMessage: 'Name of the group reporting data', + } + ), + }, + { + name: 'valueOf', + description: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.valueOfActionVariableDescription', + { + defaultMessage: + 'Record of the current value of the watched metric; grouped by condition, i.e valueOf.condition0, valueOf.condition1, etc.', + } + ), + }, + { + name: 'thresholdOf', + description: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.thresholdOfActionVariableDescription', + { + defaultMessage: + 'Record of the alerting threshold; grouped by condition, i.e thresholdOf.condition0, thresholdOf.condition1, etc.', + } + ), + }, + { + name: 'metricOf', + description: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.metricOfActionVariableDescription', + { + defaultMessage: + 'Record of the watched metric; grouped by condition, i.e metricOf.condition0, metricOf.condition1, etc.', + } + ), + }, + ], + }, +}); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts new file mode 100644 index 0000000000000..73ee1ab6b7615 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SnapshotMetricType } from '../../../../common/inventory_models/types'; + +export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold'; + +export enum Comparator { + GT = '>', + LT = '<', + GT_OR_EQ = '>=', + LT_OR_EQ = '<=', + BETWEEN = 'between', + OUTSIDE_RANGE = 'outside', +} + +export enum AlertStates { + OK, + ALERT, + NO_DATA, + ERROR, +} + +export type TimeUnit = 's' | 'm' | 'h' | 'd'; + +export interface InventoryMetricConditions { + metric: SnapshotMetricType; + timeSize: number; + timeUnit: TimeUnit; + sourceId?: string; + threshold: number[]; + comparator: Comparator; +} diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 0007b8bd719f4..2531e939792af 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -3,7 +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 { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { Comparator, AlertStates } from './types'; import * as mocks from './test_mocks'; @@ -13,81 +12,14 @@ import { AlertServicesMock, AlertInstanceMock, } from '../../../../../alerting/server/mocks'; - -const executor = createMetricThresholdExecutor('test') as (opts: { - params: AlertExecutorOptions['params']; - services: { callCluster: AlertExecutorOptions['params']['callCluster'] }; -}) => Promise; - -const services: AlertServicesMock = alertsMock.createAlertServices(); -services.callCluster.mockImplementation(async (_: string, { body, index }: any) => { - if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; - const metric = body.query.bool.filter[1]?.exists.field; - if (body.aggs.groupings) { - if (body.aggs.groupings.composite.after) { - return mocks.compositeEndResponse; - } - if (metric === 'test.metric.2') { - return mocks.alternateCompositeResponse; - } - return mocks.basicCompositeResponse; - } - if (metric === 'test.metric.2') { - return mocks.alternateMetricResponse; - } else if (metric === 'test.metric.3') { - return mocks.emptyMetricResponse; - } - return mocks.basicMetricResponse; -}); -services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { - if (sourceId === 'alternate') - return { - id: 'alternate', - attributes: { metricAlias: 'alternatebeat-*' }, - type, - references: [], - }; - return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; -}); +import { InfraSources } from '../../sources'; interface AlertTestInstance { instance: AlertInstanceMock; actionQueue: any[]; state: any; } -const alertInstances = new Map(); -services.alertInstanceFactory.mockImplementation((instanceID: string) => { - const alertInstance: AlertTestInstance = { - instance: alertsMock.createAlertInstanceFactory(), - actionQueue: [], - state: {}, - }; - alertInstances.set(instanceID, alertInstance); - alertInstance.instance.replaceState.mockImplementation((newState: any) => { - alertInstance.state = newState; - return alertInstance.instance; - }); - alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => { - alertInstance.actionQueue.push({ id, action }); - return alertInstance.instance; - }); - return alertInstance.instance; -}); - -function mostRecentAction(id: string) { - return alertInstances.get(id)!.actionQueue.pop(); -} -function getState(id: string) { - return alertInstances.get(id)!.state; -} - -const baseCriterion = { - aggType: 'avg', - metric: 'test.metric.1', - timeSize: 1, - timeUnit: 'm', -}; describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { const instanceID = 'test-*'; @@ -167,14 +99,6 @@ describe('The metric threshold alert type', () => { expect(action.reason).toContain('threshold of 0.75'); expect(action.reason).toContain('test.metric.1'); }); - test('fetches the index pattern dynamically', async () => { - await execute(Comparator.LT, [17], 'alternate'); - expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); - expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); - await execute(Comparator.LT, [1.5], 'alternate'); - expect(mostRecentAction(instanceID)).toBe(undefined); - expect(getState(instanceID).alertState).toBe(AlertStates.OK); - }); }); describe('querying with a groupBy parameter', () => { @@ -338,3 +262,117 @@ describe('The metric threshold alert type', () => { }); }); }); + +const createMockStaticConfiguration = (sources: any) => ({ + enabled: true, + query: { + partitionSize: 1, + partitionFactor: 1, + }, + sources, +}); + +const mockLibs: any = { + sources: new InfraSources({ + config: createMockStaticConfiguration({}), + }), + configuration: createMockStaticConfiguration({}), +}; + +const executor = createMetricThresholdExecutor(mockLibs, 'test') as (opts: { + params: AlertExecutorOptions['params']; + services: { callCluster: AlertExecutorOptions['params']['callCluster'] }; +}) => Promise; + +const services: AlertServicesMock = alertsMock.createAlertServices(); +services.callCluster.mockImplementation(async (_: string, { body, index }: any) => { + if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; + const metric = body.query.bool.filter[1]?.exists.field; + if (body.aggs.groupings) { + if (body.aggs.groupings.composite.after) { + return mocks.compositeEndResponse; + } + if (metric === 'test.metric.2') { + return mocks.alternateCompositeResponse; + } + return mocks.basicCompositeResponse; + } + if (metric === 'test.metric.2') { + return mocks.alternateMetricResponse; + } + return mocks.basicMetricResponse; +}); +services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { + if (sourceId === 'alternate') + return { + id: 'alternate', + attributes: { metricAlias: 'alternatebeat-*' }, + type, + references: [], + }; + return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; +}); + +services.callCluster.mockImplementation(async (_: string, { body, index }: any) => { + if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; + const metric = body.query.bool.filter[1]?.exists.field; + if (body.aggs.groupings) { + if (body.aggs.groupings.composite.after) { + return mocks.compositeEndResponse; + } + if (metric === 'test.metric.2') { + return mocks.alternateCompositeResponse; + } + return mocks.basicCompositeResponse; + } + if (metric === 'test.metric.2') { + return mocks.alternateMetricResponse; + } else if (metric === 'test.metric.3') { + return mocks.emptyMetricResponse; + } + return mocks.basicMetricResponse; +}); +services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { + if (sourceId === 'alternate') + return { + id: 'alternate', + attributes: { metricAlias: 'alternatebeat-*' }, + type, + references: [], + }; + return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; +}); + +const alertInstances = new Map(); +services.alertInstanceFactory.mockImplementation((instanceID: string) => { + const alertInstance: AlertTestInstance = { + instance: alertsMock.createAlertInstanceFactory(), + actionQueue: [], + state: {}, + }; + alertInstances.set(instanceID, alertInstance); + alertInstance.instance.replaceState.mockImplementation((newState: any) => { + alertInstance.state = newState; + return alertInstance.instance; + }); + alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => { + alertInstance.actionQueue.push({ id, action }); + return alertInstance.instance; + }); + return alertInstance.instance; +}); + +function mostRecentAction(id: string) { + return alertInstances.get(id)!.actionQueue.pop(); +} + +function getState(id: string) { + return alertInstances.get(id)!.state; +} + +const baseCriterion = { + aggType: 'avg', + metric: 'test.metric.1', + timeSize: 1, + timeUnit: 'm', +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index bd77e5e2daf42..5c34a058577a1 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -5,8 +5,6 @@ */ import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { convertSavedObjectToSavedSourceConfiguration } from '../../sources/sources'; -import { infraSourceConfigurationSavedObjectType } from '../../sources/saved_object_mappings'; import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_types'; import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler'; import { getAllCompositeData } from '../../../utils/get_all_composite_data'; @@ -22,9 +20,9 @@ import { import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getDateHistogramOffset } from '../../snapshot/query_helpers'; +import { InfraBackendLibs } from '../../infra_types'; const TOTAL_BUCKETS = 5; -const DEFAULT_INDEX_PATTERN = 'metricbeat-*'; interface Aggregation { aggregatedIntervals: { @@ -76,6 +74,7 @@ const getParsedFilterQuery: ( export const getElasticsearchMetricQuery = ( { metric, aggType, timeUnit, timeSize }: MetricExpressionParams, + timefield: string, groupBy?: string, filterQuery?: string ) => { @@ -109,7 +108,7 @@ export const getElasticsearchMetricQuery = ( const baseAggs = { aggregatedIntervals: { date_histogram: { - field: '@timestamp', + field: timefield, fixed_interval: interval, offset, extended_bounds: { @@ -181,43 +180,23 @@ export const getElasticsearchMetricQuery = ( }; }; -const getIndexPattern: ( - services: AlertServices, - sourceId?: string -) => Promise = async function({ savedObjectsClient }, sourceId = 'default') { - try { - const sourceConfiguration = await savedObjectsClient.get( - infraSourceConfigurationSavedObjectType, - sourceId - ); - const { metricAlias } = convertSavedObjectToSavedSourceConfiguration( - sourceConfiguration - ).configuration; - return metricAlias || DEFAULT_INDEX_PATTERN; - } catch (e) { - if (e.output.statusCode === 404) { - return DEFAULT_INDEX_PATTERN; - } else { - throw e; - } - } -}; - const getMetric: ( services: AlertServices, params: MetricExpressionParams, index: string, + timefield: string, groupBy: string | undefined, filterQuery: string | undefined ) => Promise> = async function( - { savedObjectsClient, callCluster }, + { callCluster }, params, index, + timefield, groupBy, filterQuery ) { const { aggType } = params; - const searchBody = getElasticsearchMetricQuery(params, groupBy, filterQuery); + const searchBody = getElasticsearchMetricQuery(params, timefield, groupBy, filterQuery); try { if (groupBy) { @@ -265,7 +244,7 @@ const comparatorMap = { [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b, }; -export const createMetricThresholdExecutor = (alertUUID: string) => +export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: string) => async function({ services, params }: AlertExecutorOptions) { const { criteria, groupBy, filterQuery, sourceId, alertOnNoData } = params as { criteria: MetricExpressionParams[]; @@ -275,11 +254,22 @@ export const createMetricThresholdExecutor = (alertUUID: string) => alertOnNoData: boolean; }; + const source = await libs.sources.getSourceConfiguration( + services.savedObjectsClient, + sourceId || 'default' + ); + const config = source.configuration; const alertResults = await Promise.all( - criteria.map(criterion => - (async () => { - const index = await getIndexPattern(services, sourceId); - const currentValues = await getMetric(services, criterion, index, groupBy, filterQuery); + criteria.map(criterion => { + return (async () => { + const currentValues = await getMetric( + services, + criterion, + config.fields.timestamp, + config.metricAlias, + groupBy, + filterQuery + ); const { threshold, comparator } = criterion; const comparisonFunction = comparatorMap[comparator]; return mapValues(currentValues, value => ({ @@ -291,13 +281,14 @@ export const createMetricThresholdExecutor = (alertUUID: string) => isNoData: value === null, isError: value === undefined, })); - })() - ) + })(); + }) ); + // Because each alert result has the same group definitions, just grap the groups from the first one. const groups = Object.keys(alertResults[0]); for (const group of groups) { - const alertInstance = services.alertInstanceFactory(`${alertUUID}-${group}`); + const alertInstance = services.alertInstanceFactory(`${alertId}-${group}`); // AND logic; all criteria must be across the threshold const shouldAlertFire = alertResults.every(result => result[group].shouldFire); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 029491c1168cf..23611559a184f 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -6,11 +6,11 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; -import { PluginSetupContract } from '../../../../../alerting/server'; +import { curry } from 'lodash'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; -import { InfraBackendLibs } from '../../infra_types'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; +import { InfraBackendLibs } from '../../infra_types'; const oneOfLiterals = (arrayOfLiterals: Readonly) => schema.string({ @@ -18,17 +18,7 @@ const oneOfLiterals = (arrayOfLiterals: Readonly) => arrayOfLiterals.includes(value) ? undefined : `must be one of ${arrayOfLiterals.join(' | ')}`, }); -export async function registerMetricThresholdAlertType( - alertingPlugin: PluginSetupContract, - libs: InfraBackendLibs -) { - if (!alertingPlugin) { - throw new Error( - 'Cannot register metric threshold alert type. Both the actions and alerting plugins need to be enabled.' - ); - } - const alertUUID = uuid.v4(); - +export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { const baseCriterion = { threshold: schema.arrayOf(schema.number()), comparator: oneOfLiterals(Object.values(Comparator)), @@ -70,21 +60,24 @@ export async function registerMetricThresholdAlertType( } ); - alertingPlugin.registerType({ + return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, name: 'Metric threshold', validate: { - params: schema.object({ - criteria: schema.arrayOf(schema.oneOf([countCriterion, nonCountCriterion])), - groupBy: schema.maybe(schema.string()), - filterQuery: schema.maybe(schema.string()), - sourceId: schema.string(), - alertOnNoData: schema.maybe(schema.boolean()), - }), + params: schema.object( + { + criteria: schema.arrayOf(schema.oneOf([countCriterion, nonCountCriterion])), + groupBy: schema.maybe(schema.string()), + filterQuery: schema.maybe(schema.string()), + sourceId: schema.string(), + alertOnNoData: schema.maybe(schema.boolean()), + }, + { unknowns: 'allow' } + ), }, defaultActionGroupId: FIRED_ACTIONS.id, actionGroups: [FIRED_ACTIONS], - executor: createMetricThresholdExecutor(alertUUID), + executor: curry(createMetricThresholdExecutor)(libs, uuid.v4()), actionVariables: { context: [ { name: 'group', description: groupActionVariableDescription }, @@ -92,5 +85,5 @@ export async function registerMetricThresholdAlertType( { name: 'reason', description: reasonActionVariableDescription }, ], }, - }); + }; } diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts index 9760873ff7478..44d30d7281f20 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -6,13 +6,16 @@ import { PluginSetupContract } from '../../../../alerting/server'; import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type'; +import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type'; import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type'; import { InfraBackendLibs } from '../infra_types'; const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => { if (alertingPlugin) { - const registerFns = [registerMetricThresholdAlertType, registerLogThresholdAlertType]; + alertingPlugin.registerType(registerMetricThresholdAlertType(libs)); + alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs)); + const registerFns = [registerLogThresholdAlertType]; registerFns.forEach(fn => { fn(alertingPlugin, libs); }); diff --git a/x-pack/plugins/infra/server/lib/compose/kibana.ts b/x-pack/plugins/infra/server/lib/compose/kibana.ts index f100726b5b92e..d22ca2961cfa5 100644 --- a/x-pack/plugins/infra/server/lib/compose/kibana.ts +++ b/x-pack/plugins/infra/server/lib/compose/kibana.ts @@ -28,7 +28,7 @@ export function compose(core: CoreSetup, config: InfraConfig, plugins: InfraServ const sourceStatus = new InfraSourceStatus(new InfraElasticsearchSourceStatusAdapter(framework), { sources, }); - const snapshot = new InfraSnapshot({ sources, framework }); + const snapshot = new InfraSnapshot(); const logEntryCategoriesAnalysis = new LogEntryCategoriesAnalysis({ framework }); const logEntryRateAnalysis = new LogEntryRateAnalysis({ framework }); diff --git a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts index cf2b1e59b2a22..c75ee6d644044 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts @@ -5,26 +5,23 @@ */ import { uniq } from 'lodash'; -import { RequestHandlerContext } from 'kibana/server'; import { InfraSnapshotRequestOptions } from './types'; import { getMetricsAggregations } from './query_helpers'; import { calculateMetricInterval } from '../../utils/calculate_metric_interval'; import { SnapshotModel, SnapshotModelMetricAggRT } from '../../../common/inventory_models/types'; -import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { getDatasetForField } from '../../routes/metrics_explorer/lib/get_dataset_for_field'; import { InfraTimerangeInput } from '../../../common/http_api/snapshot_api'; +import { ESSearchClient } from '.'; export const createTimeRangeWithInterval = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + client: ESSearchClient, options: InfraSnapshotRequestOptions ): Promise => { const aggregations = getMetricsAggregations(options); - const modules = await aggregationsToModules(framework, requestContext, aggregations, options); + const modules = await aggregationsToModules(client, aggregations, options); const interval = Math.max( (await calculateMetricInterval( - framework, - requestContext, + client, { indexPattern: options.sourceConfiguration.metricAlias, timestampField: options.sourceConfiguration.fields.timestamp, @@ -43,8 +40,7 @@ export const createTimeRangeWithInterval = async ( }; const aggregationsToModules = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + client: ESSearchClient, aggregations: SnapshotModel, options: InfraSnapshotRequestOptions ): Promise => { @@ -59,12 +55,7 @@ const aggregationsToModules = async ( const fields = await Promise.all( uniqueFields.map( async field => - await getDatasetForField( - framework, - requestContext, - field as string, - options.sourceConfiguration.metricAlias - ) + await getDatasetForField(client, field as string, options.sourceConfiguration.metricAlias) ) ); return fields.filter(f => f) as string[]; diff --git a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts index 07abfa5fd474a..4057ed246ccaf 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts @@ -3,11 +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 { RequestHandlerContext } from 'src/core/server'; -import { InfraDatabaseSearchResponse } from '../adapters/framework'; -import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; -import { InfraSources } from '../sources'; +import { InfraDatabaseSearchResponse, CallWithRequestParams } from '../adapters/framework'; import { JsonObject } from '../../../common/typed_json'; import { SNAPSHOT_COMPOSITE_REQUEST_SIZE } from './constants'; @@ -31,36 +27,26 @@ import { InfraSnapshotRequestOptions } from './types'; import { createTimeRangeWithInterval } from './create_timerange_with_interval'; import { SnapshotNode } from '../../../common/http_api/snapshot_api'; +export type ESSearchClient = ( + options: CallWithRequestParams +) => Promise>; export class InfraSnapshot { - constructor(private readonly libs: { sources: InfraSources; framework: KibanaFramework }) {} - public async getNodes( - requestContext: RequestHandlerContext, + client: ESSearchClient, options: InfraSnapshotRequestOptions ): Promise<{ nodes: SnapshotNode[]; interval: string }> { // Both requestGroupedNodes and requestNodeMetrics may send several requests to elasticsearch // in order to page through the results of their respective composite aggregations. // Both chains of requests are supposed to run in parallel, and their results be merged // when they have both been completed. - const timeRangeWithIntervalApplied = await createTimeRangeWithInterval( - this.libs.framework, - requestContext, - options - ); + const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, options); const optionsWithTimerange = { ...options, timerange: timeRangeWithIntervalApplied }; - const groupedNodesPromise = requestGroupedNodes( - requestContext, - optionsWithTimerange, - this.libs.framework - ); - const nodeMetricsPromise = requestNodeMetrics( - requestContext, - optionsWithTimerange, - this.libs.framework - ); + const groupedNodesPromise = requestGroupedNodes(client, optionsWithTimerange); + const nodeMetricsPromise = requestNodeMetrics(client, optionsWithTimerange); const groupedNodeBuckets = await groupedNodesPromise; const nodeMetricBuckets = await nodeMetricsPromise; + return { nodes: mergeNodeBuckets(groupedNodeBuckets, nodeMetricBuckets, options), interval: timeRangeWithIntervalApplied.interval, @@ -77,15 +63,12 @@ const handleAfterKey = createAfterKeyHandler( input => input?.aggregations?.nodes?.after_key ); -const callClusterFactory = (framework: KibanaFramework, requestContext: RequestHandlerContext) => ( - opts: any -) => - framework.callWithRequest<{}, InfraSnapshotAggregationResponse>(requestContext, 'search', opts); +const callClusterFactory = (search: ESSearchClient) => (opts: any) => + search<{}, InfraSnapshotAggregationResponse>(opts); const requestGroupedNodes = async ( - requestContext: RequestHandlerContext, - options: InfraSnapshotRequestOptions, - framework: KibanaFramework + client: ESSearchClient, + options: InfraSnapshotRequestOptions ): Promise => { const inventoryModel = findInventoryModel(options.nodeType); const query = { @@ -124,13 +107,12 @@ const requestGroupedNodes = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeGroupByBucket - >(callClusterFactory(framework, requestContext), query, bucketSelector, handleAfterKey); + >(callClusterFactory(client), query, bucketSelector, handleAfterKey); }; const requestNodeMetrics = async ( - requestContext: RequestHandlerContext, - options: InfraSnapshotRequestOptions, - framework: KibanaFramework + client: ESSearchClient, + options: InfraSnapshotRequestOptions ): Promise => { const index = options.metric.type === 'logRate' @@ -175,7 +157,7 @@ const requestNodeMetrics = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeMetricsBucket - >(callClusterFactory(framework, requestContext), query, bucketSelector, handleAfterKey); + >(callClusterFactory(client), query, bucketSelector, handleAfterKey); }; // buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[] diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 0368c7bfd6db8..71682c9e798a6 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -9,7 +9,7 @@ import { failure } from 'io-ts/lib/PathReporter'; import { identity, constant } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; -import { RequestHandlerContext, SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { defaultSourceConfiguration } from './defaults'; import { NotFoundError } from './errors'; import { infraSourceConfigurationSavedObjectType } from './saved_object_mappings'; @@ -41,7 +41,6 @@ export class InfraSources { sourceId: string ): Promise { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const savedSourceConfiguration = await this.getInternalSourceConfiguration(sourceId) .then(internalSourceConfiguration => ({ id: sourceId, @@ -79,10 +78,12 @@ export class InfraSources { return savedSourceConfiguration; } - public async getAllSourceConfigurations(requestContext: RequestHandlerContext) { + public async getAllSourceConfigurations(savedObjectsClient: SavedObjectsClientContract) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(requestContext); + const savedSourceConfigurations = await this.getAllSavedSourceConfigurations( + savedObjectsClient + ); return savedSourceConfigurations.map(savedSourceConfiguration => ({ ...savedSourceConfiguration, @@ -94,7 +95,7 @@ export class InfraSources { } public async createSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string, source: InfraSavedSourceConfiguration ) { @@ -106,7 +107,7 @@ export class InfraSources { ); const createdSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await requestContext.core.savedObjects.client.create( + await savedObjectsClient.create( infraSourceConfigurationSavedObjectType, pickSavedSourceConfiguration(newSourceConfiguration) as any, { id: sourceId } @@ -122,22 +123,22 @@ export class InfraSources { }; } - public async deleteSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) { - await requestContext.core.savedObjects.client.delete( - infraSourceConfigurationSavedObjectType, - sourceId - ); + public async deleteSourceConfiguration( + savedObjectsClient: SavedObjectsClientContract, + sourceId: string + ) { + await savedObjectsClient.delete(infraSourceConfigurationSavedObjectType, sourceId); } public async updateSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string, sourceProperties: InfraSavedSourceConfiguration ) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); const { configuration, version } = await this.getSourceConfiguration( - requestContext.core.savedObjects.client, + savedObjectsClient, sourceId ); @@ -147,7 +148,7 @@ export class InfraSources { ); const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await requestContext.core.savedObjects.client.update( + await savedObjectsClient.update( infraSourceConfigurationSavedObjectType, sourceId, pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any, @@ -213,8 +214,8 @@ export class InfraSources { return convertSavedObjectToSavedSourceConfiguration(savedObject); } - private async getAllSavedSourceConfigurations(requestContext: RequestHandlerContext) { - const savedObjects = await requestContext.core.savedObjects.client.find({ + private async getAllSavedSourceConfigurations(savedObjectsClient: SavedObjectsClientContract) { + const savedObjects = await savedObjectsClient.find({ type: infraSourceConfigurationSavedObjectType, }); diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index d4dfa60ac67a0..db34033c1d4f8 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -109,7 +109,7 @@ export class InfraServerPlugin { sources, } ); - const snapshot = new InfraSnapshot({ sources, framework }); + const snapshot = new InfraSnapshot(); const logEntryCategoriesAnalysis = new LogEntryCategoriesAnalysis({ framework }); const logEntryRateAnalysis = new LogEntryRateAnalysis({ framework }); diff --git a/x-pack/plugins/infra/server/routes/log_sources/configuration.ts b/x-pack/plugins/infra/server/routes/log_sources/configuration.ts index 0ce594675773c..46929954431f5 100644 --- a/x-pack/plugins/infra/server/routes/log_sources/configuration.ts +++ b/x-pack/plugins/infra/server/routes/log_sources/configuration.ts @@ -82,12 +82,12 @@ export const initLogSourceConfigurationRoutes = ({ framework, sources }: InfraBa const sourceConfigurationExists = sourceConfiguration.origin === 'stored'; const patchedSourceConfiguration = await (sourceConfigurationExists ? sources.updateSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId, patchedSourceConfigurationProperties ) : sources.createSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId, patchedSourceConfigurationProperties )); diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts index 66f0ca8fc706a..94e91d32b14bb 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext } from 'kibana/server'; -import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; +import { ESSearchClient } from '../../../lib/snapshot'; interface EventDatasetHit { _source: { @@ -16,8 +15,7 @@ interface EventDatasetHit { } export const getDatasetForField = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + client: ESSearchClient, field: string, indexPattern: string ) => { @@ -33,11 +31,8 @@ export const getDatasetForField = async ( }, }; - const response = await framework.callWithRequest( - requestContext, - 'search', - params - ); + const response = await client(params); + if (response.hits.total.value === 0) { return null; } diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts index e735a26d96a91..a709cbdeeb680 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts @@ -17,6 +17,10 @@ import { createMetricModel } from './create_metrics_model'; import { JsonObject } from '../../../../common/typed_json'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; import { getDatasetForField } from './get_dataset_for_field'; +import { + CallWithRequestParams, + InfraDatabaseSearchResponse, +} from '../../../lib/adapters/framework'; export const populateSeriesWithTSVBData = ( request: KibanaRequest, @@ -52,17 +56,21 @@ export const populateSeriesWithTSVBData = ( } const timerange = { min: options.timerange.from, max: options.timerange.to }; + const client = ( + opts: CallWithRequestParams + ): Promise> => + framework.callWithRequest(requestContext, 'search', opts); + // Create the TSVB model based on the request options const model = createMetricModel(options); const modules = await Promise.all( uniq(options.metrics.filter(m => m.field)).map( - async m => - await getDatasetForField(framework, requestContext, m.field as string, options.indexPattern) + async m => await getDatasetForField(client, m.field as string, options.indexPattern) ) ); + const calculatedInterval = await calculateMetricInterval( - framework, - requestContext, + client, { indexPattern: options.indexPattern, timestampField: options.timerange.field, diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index d1dc03893a0d9..2d951d426b03a 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -13,6 +13,7 @@ import { UsageCollector } from '../../usage/usage_collector'; import { parseFilterQuery } from '../../utils/serialized_query'; import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api'; import { throwErrors } from '../../../common/runtime_types'; +import { CallWithRequestParams, InfraDatabaseSearchResponse } from '../../lib/adapters/framework'; const escapeHatch = schema.object({}, { unknowns: 'allow' }); @@ -57,7 +58,13 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { metric, timerange, }; - const nodesWithInterval = await libs.snapshot.getNodes(requestContext, options); + + const searchES = ( + opts: CallWithRequestParams + ): Promise> => + framework.callWithRequest(requestContext, 'search', opts); + + const nodesWithInterval = await libs.snapshot.getNodes(searchES, options); return response.ok({ body: SnapshotNodeResponseRT.encode(nodesWithInterval), }); diff --git a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts index 7cbbdc0f2145b..43e109b009f48 100644 --- a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts +++ b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext } from 'src/core/server'; +// import { RequestHandlerContext } from 'src/core/server'; import { findInventoryModel } from '../../common/inventory_models'; -import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; +// import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; import { InventoryItemType } from '../../common/inventory_models/types'; +import { ESSearchClient } from '../lib/snapshot'; interface Options { indexPattern: string; @@ -23,8 +24,7 @@ interface Options { * This is useful for visualizing metric modules like s3 that only send metrics once per day. */ export const calculateMetricInterval = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + client: ESSearchClient, options: Options, modules?: string[], nodeType?: InventoryItemType // TODO: check that this type still makes sense @@ -73,11 +73,7 @@ export const calculateMetricInterval = async ( }, }; - const resp = await framework.callWithRequest<{}, PeriodAggregationData>( - requestContext, - 'search', - query - ); + const resp = await client<{}, PeriodAggregationData>(query); // if ES doesn't return an aggregations key, something went seriously wrong. if (!resp.aggregations) { diff --git a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts index 19879f5761ab2..5c43e8938a8c1 100644 --- a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts +++ b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts @@ -32,7 +32,7 @@ export default function({ getService }: FtrProviderContext) { describe('querying the entire infrastructure', () => { for (const aggType of aggs) { it(`should work with the ${aggType} aggregator`, async () => { - const searchBody = getElasticsearchMetricQuery(getSearchParams(aggType)); + const searchBody = getElasticsearchMetricQuery(getSearchParams(aggType), '@timestamp'); const result = await client.search({ index, body: searchBody, @@ -45,6 +45,7 @@ export default function({ getService }: FtrProviderContext) { it('should work with a filterQuery', async () => { const searchBody = getElasticsearchMetricQuery( getSearchParams('avg'), + '@timestamp', undefined, '{"bool":{"should":[{"match_phrase":{"agent.hostname":"foo"}}],"minimum_should_match":1}}' ); @@ -59,6 +60,7 @@ export default function({ getService }: FtrProviderContext) { it('should work with a filterQuery in KQL format', async () => { const searchBody = getElasticsearchMetricQuery( getSearchParams('avg'), + '@timestamp', undefined, '"agent.hostname":"foo"' ); @@ -74,7 +76,11 @@ export default function({ getService }: FtrProviderContext) { describe('querying with a groupBy parameter', () => { for (const aggType of aggs) { it(`should work with the ${aggType} aggregator`, async () => { - const searchBody = getElasticsearchMetricQuery(getSearchParams(aggType), 'agent.id'); + const searchBody = getElasticsearchMetricQuery( + getSearchParams(aggType), + '@timestamp', + 'agent.id' + ); const result = await client.search({ index, body: searchBody, @@ -87,6 +93,7 @@ export default function({ getService }: FtrProviderContext) { it('should work with a filterQuery', async () => { const searchBody = getElasticsearchMetricQuery( getSearchParams('avg'), + '@timestamp', 'agent.id', '{"bool":{"should":[{"match_phrase":{"agent.hostname":"foo"}}],"minimum_should_match":1}}' ); From ffe5166023a8e4d82d5b4fead3718d69a0dc62bc Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Thu, 30 Apr 2020 17:54:37 -0400 Subject: [PATCH 058/122] [EPM] restrict package install endpoint from installing/updating to old packages (#64932) * restrict installing or updating to out-of-date package * throw bad requests in remove handler * remove accidental commit * remove space --- .../ingest_manager/server/routes/epm/handlers.ts | 12 ++++++++++++ .../server/services/epm/packages/install.ts | 12 ++++++++++-- .../server/services/epm/packages/remove.ts | 5 +++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index ad16e1dde456b..fd3a9c520b90a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -136,6 +136,12 @@ export const installPackageHandler: RequestHandler Date: Thu, 30 Apr 2020 18:46:07 -0400 Subject: [PATCH 059/122] [Discover] Show doc viewer action buttons on focus (#64912) --- .../public/components/doc_viewer/_doc_viewer.scss | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/plugins/discover/public/components/doc_viewer/_doc_viewer.scss b/src/plugins/discover/public/components/doc_viewer/_doc_viewer.scss index 25aa530976719..ec2beca15a546 100644 --- a/src/plugins/discover/public/components/doc_viewer/_doc_viewer.scss +++ b/src/plugins/discover/public/components/doc_viewer/_doc_viewer.scss @@ -43,6 +43,14 @@ } .kbnDocViewer__buttons { width: 60px; + + // Show all icons if one is focused, + // IE doesn't support, but the fallback is just the focused button becomes visible + &:focus-within { + .kbnDocViewer__actionButton { + opacity: 1; + } + } } .kbnDocViewer__field { @@ -51,7 +59,12 @@ .kbnDocViewer__actionButton { opacity: 0; + + &:focus { + opacity: 1; + } } + .kbnDocViewer__warning { margin-right: $euiSizeS; } From 7e5be981d8ac4deacea9cd03e21e168f279413be Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 30 Apr 2020 18:07:23 -0700 Subject: [PATCH 060/122] Fixed `AddAlert` flyout does not immediately update state to reflect new props (#64927) * Fixed `AddAlert` flyout does not immediately update state to reflect new props * fixed add form * Fixed type check --- .../builtin_action_types/es_index.tsx | 4 +- .../sections/alert_form/alert_add.tsx | 9 +++- .../sections/alert_form/alert_edit.tsx | 5 ++ .../apps/triggers_actions_ui/details.ts | 47 +++++++++++++++++++ 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx index 55a219ca94aea..861d6ad7284c2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx @@ -86,7 +86,9 @@ const IndexActionConnectorFields: React.FunctionComponent([]); - const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); + const [timeFieldOptions, setTimeFieldOptions] = useState>([ + firstFieldOption, + ]); const [isIndiciesLoading, setIsIndiciesLoading] = useState(false); useEffect(() => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 0620ced6365a9..651f2cdba34af 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -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 React, { useCallback, useReducer, useState } from 'react'; +import React, { useCallback, useReducer, useState, useEffect } from 'react'; import { isObject } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -60,6 +60,9 @@ export const AlertAdd = ({ const setAlert = (value: any) => { dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); }; + const setAlertProperty = (key: string, value: any) => { + dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); + }; const { reloadAlerts, @@ -70,6 +73,10 @@ export const AlertAdd = ({ docLinks, } = useAlertsContext(); + useEffect(() => { + setAlertProperty('alertTypeId', alertTypeId); + }, [alertTypeId]); + const closeFlyout = useCallback(() => { setAddFlyoutVisibility(false); setAlert(initialAlert); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 4255eca83be47..c9cf59d87414f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -43,6 +43,9 @@ export const AlertEdit = ({ const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); const [isSaving, setIsSaving] = useState(false); const [hasActionsDisabled, setHasActionsDisabled] = useState(false); + const setAlert = (key: string, value: any) => { + dispatch({ command: { type: 'setAlert' }, payload: { key, value } }); + }; const { reloadAlerts, @@ -55,6 +58,8 @@ export const AlertEdit = ({ const closeFlyout = useCallback(() => { setEditFlyoutVisibility(false); + setAlert('alert', initialAlert); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [setEditFlyoutVisibility]); if (!editFlyoutVisible) { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index d0ce18bbc1c54..6ff065c1f4ab2 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -197,6 +197,53 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const headingText = await pageObjects.alertDetailsUI.getHeadingText(); expect(headingText).to.be(updatedAlertName); }); + + it('should reset alert when canceling an edit', async () => { + await pageObjects.common.navigateToApp('triggersActions'); + const params = { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000, 5000], + index: ['.kibana_1'], + timeField: 'alert', + }; + const alert = await alerting.alerts.createAlertWithActions( + testRunUuid, + '.index-threshold', + params + ); + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + + const editButton = await testSubjects.find('openEditAlertFlyoutButton'); + await editButton.click(); + + const updatedAlertName = `Changed Alert Name ${uuid.v4()}`; + await testSubjects.setValue('alertNameInput', updatedAlertName, { + clearWithKeyboard: true, + }); + + await testSubjects.click('cancelSaveEditedAlertButton'); + await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]'); + + await editButton.click(); + + const nameInputAfterCancel = await testSubjects.find('alertNameInput'); + const textAfterCancel = await nameInputAfterCancel.getAttribute('value'); + expect(textAfterCancel).to.eql(alert.name); + }); }); describe('View In App', function() { From 7f8f765541cf31f3efb59d76ff587069a3dc95d3 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Thu, 30 Apr 2020 19:40:01 -0600 Subject: [PATCH 061/122] [data.search.aggs] Remove legacy aggs APIs. (#64719) --- ...ugin-plugins-data-public.agggrouplabels.md | 15 +++ ...plugin-plugins-data-public.agggroupname.md | 11 ++ ...ta-public.aggtypefieldfilters.addfilter.md | 24 ---- ...-data-public.aggtypefieldfilters.filter.md | 25 ---- ...plugins-data-public.aggtypefieldfilters.md | 21 ---- ...ns-data-public.aggtypefilters.addfilter.md | 24 ---- ...ugins-data-public.aggtypefilters.filter.md | 27 ----- ...ugin-plugins-data-public.aggtypefilters.md | 21 ---- ...n-plugins-data-public.daterangekey.from.md | 11 -- ...plugin-plugins-data-public.daterangekey.md | 19 --- ...gin-plugins-data-public.daterangekey.to.md | 11 -- ...ugin-plugins-data-public.iagggroupnames.md | 11 -- ...a-plugin-plugins-data-public.iprangekey.md | 18 --- .../kibana-plugin-plugins-data-public.md | 8 +- ...ublic.optionedparameditorprops.aggparam.md | 13 -- ...ns-data-public.optionedparameditorprops.md | 18 --- ...ibana-plugin-plugins-data-public.search.md | 5 - .../new_platform/new_platform.karma_mock.js | 10 -- src/plugins/data/public/index.ts | 30 ++--- src/plugins/data/public/public.api.md | 112 +++++------------- .../data/public/search/aggs/agg_groups.ts | 14 ++- .../aggs/filter/agg_type_filters.test.ts | 62 ---------- .../search/aggs/filter/agg_type_filters.ts | 74 ------------ .../data/public/search/aggs/filter/index.ts | 21 ---- src/plugins/data/public/search/aggs/index.ts | 1 - .../public/search/aggs/param_types/field.ts | 2 +- .../param_types/filter/field_filters.test.ts | 61 ---------- .../aggs/param_types/filter/field_filters.ts | 60 ---------- .../public/search/aggs/param_types/index.ts | 1 - .../search/aggs/param_types/optioned.ts | 6 - src/plugins/data/public/search/aggs/types.ts | 20 +--- .../data/public/search/aggs/utils/index.ts | 1 + .../{filter => utils}/prop_filter.test.ts | 0 .../aggs/{filter => utils}/prop_filter.ts | 4 +- src/plugins/data/public/search/mocks.ts | 8 -- .../data/public/search/search_service.ts | 14 --- src/plugins/data/public/search/types.ts | 4 +- .../saved_objects_table.test.tsx.snap | 9 -- .../agg_filters/agg_type_field_filters.ts | 49 ++++++++ .../public/agg_filters/agg_type_filters.ts | 75 ++++++++++++ .../public/agg_filters}/index.ts | 3 +- .../public/components/agg_common_props.ts | 4 +- .../public/components/agg_group.tsx | 4 +- .../public/components/agg_param_props.ts | 13 +- .../public/components/agg_params.tsx | 16 +-- .../components/agg_params_helper.test.ts | 34 +----- .../public/components/agg_params_helper.ts | 24 ++-- .../public/components/controls/order.tsx | 4 +- .../components/controls/top_aggregate.tsx | 3 +- .../public/default_editor.tsx | 1 - .../vis_default_editor/public/schemas.ts | 4 +- .../public/vis_type_agg_filter.ts | 33 ------ x-pack/plugins/rollup/kibana.json | 3 +- x-pack/plugins/rollup/public/plugin.ts | 18 +-- .../public/visualize/agg_type_field_filter.js | 22 ---- .../public/visualize/agg_type_filter.js | 23 ---- 56 files changed, 246 insertions(+), 883 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggrouplabels.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggroupname.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.addfilter.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.filter.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.addfilter.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.filter.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.from.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.to.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iagggroupnames.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iprangekey.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.aggparam.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.md delete mode 100644 src/plugins/data/public/search/aggs/filter/agg_type_filters.test.ts delete mode 100644 src/plugins/data/public/search/aggs/filter/agg_type_filters.ts delete mode 100644 src/plugins/data/public/search/aggs/filter/index.ts delete mode 100644 src/plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts delete mode 100644 src/plugins/data/public/search/aggs/param_types/filter/field_filters.ts rename src/plugins/data/public/search/aggs/{filter => utils}/prop_filter.test.ts (100%) rename src/plugins/data/public/search/aggs/{filter => utils}/prop_filter.ts (97%) create mode 100644 src/plugins/vis_default_editor/public/agg_filters/agg_type_field_filters.ts create mode 100644 src/plugins/vis_default_editor/public/agg_filters/agg_type_filters.ts rename src/plugins/{data/public/search/aggs/param_types/filter => vis_default_editor/public/agg_filters}/index.ts (91%) delete mode 100644 src/plugins/vis_default_editor/public/vis_type_agg_filter.ts delete mode 100644 x-pack/plugins/rollup/public/visualize/agg_type_field_filter.js delete mode 100644 x-pack/plugins/rollup/public/visualize/agg_type_filter.js diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggrouplabels.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggrouplabels.md new file mode 100644 index 0000000000000..6684ba8546f85 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggrouplabels.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggGroupLabels](./kibana-plugin-plugins-data-public.agggrouplabels.md) + +## AggGroupLabels variable + +Signature: + +```typescript +AggGroupLabels: { + [AggGroupNames.Buckets]: string; + [AggGroupNames.Metrics]: string; + [AggGroupNames.None]: string; +} +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggroupname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggroupname.md new file mode 100644 index 0000000000000..d4476398680a8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggroupname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggGroupName](./kibana-plugin-plugins-data-public.agggroupname.md) + +## AggGroupName type + +Signature: + +```typescript +export declare type AggGroupName = $Values; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.addfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.addfilter.md deleted file mode 100644 index c9d6772a13b8d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.addfilter.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggTypeFieldFilters](./kibana-plugin-plugins-data-public.aggtypefieldfilters.md) > [addFilter](./kibana-plugin-plugins-data-public.aggtypefieldfilters.addfilter.md) - -## AggTypeFieldFilters.addFilter() method - -Register a new with this registry. This will be used by the . - -Signature: - -```typescript -addFilter(filter: AggTypeFieldFilter): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| filter | AggTypeFieldFilter | | - -Returns: - -`void` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.filter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.filter.md deleted file mode 100644 index 038c339bf6774..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.filter.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggTypeFieldFilters](./kibana-plugin-plugins-data-public.aggtypefieldfilters.md) > [filter](./kibana-plugin-plugins-data-public.aggtypefieldfilters.filter.md) - -## AggTypeFieldFilters.filter() method - -Returns the filtered by all registered filters. - -Signature: - -```typescript -filter(fields: IndexPatternField[], aggConfig: IAggConfig): IndexPatternField[]; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| fields | IndexPatternField[] | | -| aggConfig | IAggConfig | | - -Returns: - -`IndexPatternField[]` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.md deleted file mode 100644 index c0b386efbf9c7..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggTypeFieldFilters](./kibana-plugin-plugins-data-public.aggtypefieldfilters.md) - -## AggTypeFieldFilters class - -A registry to store which are used to filter down available fields for a specific visualization and . - -Signature: - -```typescript -declare class AggTypeFieldFilters -``` - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [addFilter(filter)](./kibana-plugin-plugins-data-public.aggtypefieldfilters.addfilter.md) | | Register a new with this registry. This will be used by the . | -| [filter(fields, aggConfig)](./kibana-plugin-plugins-data-public.aggtypefieldfilters.filter.md) | | Returns the filtered by all registered filters. | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.addfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.addfilter.md deleted file mode 100644 index 9df003377c4a1..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.addfilter.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggTypeFilters](./kibana-plugin-plugins-data-public.aggtypefilters.md) > [addFilter](./kibana-plugin-plugins-data-public.aggtypefilters.addfilter.md) - -## AggTypeFilters.addFilter() method - -Register a new with this registry. - -Signature: - -```typescript -addFilter(filter: AggTypeFilter): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| filter | AggTypeFilter | | - -Returns: - -`void` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.filter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.filter.md deleted file mode 100644 index 81e6e9b95d655..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.filter.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggTypeFilters](./kibana-plugin-plugins-data-public.aggtypefilters.md) > [filter](./kibana-plugin-plugins-data-public.aggtypefilters.filter.md) - -## AggTypeFilters.filter() method - -Returns the filtered by all registered filters. - -Signature: - -```typescript -filter(aggTypes: IAggType[], indexPattern: IndexPattern, aggConfig: IAggConfig, aggFilter: string[]): IAggType[]; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| aggTypes | IAggType[] | | -| indexPattern | IndexPattern | | -| aggConfig | IAggConfig | | -| aggFilter | string[] | | - -Returns: - -`IAggType[]` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.md deleted file mode 100644 index c5e24bc0a78a0..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggTypeFilters](./kibana-plugin-plugins-data-public.aggtypefilters.md) - -## AggTypeFilters class - -A registry to store which are used to filter down available aggregations for a specific visualization and . - -Signature: - -```typescript -declare class AggTypeFilters -``` - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [addFilter(filter)](./kibana-plugin-plugins-data-public.aggtypefilters.addfilter.md) | | Register a new with this registry. | -| [filter(aggTypes, indexPattern, aggConfig, aggFilter)](./kibana-plugin-plugins-data-public.aggtypefilters.filter.md) | | Returns the filtered by all registered filters. | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.from.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.from.md deleted file mode 100644 index 245269af366bc..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.from.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DateRangeKey](./kibana-plugin-plugins-data-public.daterangekey.md) > [from](./kibana-plugin-plugins-data-public.daterangekey.from.md) - -## DateRangeKey.from property - -Signature: - -```typescript -from: number; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.md deleted file mode 100644 index 540d429dced48..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DateRangeKey](./kibana-plugin-plugins-data-public.daterangekey.md) - -## DateRangeKey interface - -Signature: - -```typescript -export interface DateRangeKey -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [from](./kibana-plugin-plugins-data-public.daterangekey.from.md) | number | | -| [to](./kibana-plugin-plugins-data-public.daterangekey.to.md) | number | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.to.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.to.md deleted file mode 100644 index 024a6c2105427..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.to.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DateRangeKey](./kibana-plugin-plugins-data-public.daterangekey.md) > [to](./kibana-plugin-plugins-data-public.daterangekey.to.md) - -## DateRangeKey.to property - -Signature: - -```typescript -to: number; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iagggroupnames.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iagggroupnames.md deleted file mode 100644 index 07310a4219359..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iagggroupnames.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IAggGroupNames](./kibana-plugin-plugins-data-public.iagggroupnames.md) - -## IAggGroupNames type - -Signature: - -```typescript -export declare type IAggGroupNames = $Values; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iprangekey.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iprangekey.md deleted file mode 100644 index 96903a5df9844..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iprangekey.md +++ /dev/null @@ -1,18 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IpRangeKey](./kibana-plugin-plugins-data-public.iprangekey.md) - -## IpRangeKey type - -Signature: - -```typescript -export declare type IpRangeKey = { - type: 'mask'; - mask: string; -} | { - type: 'range'; - from: string; - to: string; -}; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index e1df493143b73..13e38ba5e6e5d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -9,8 +9,6 @@ | Class | Description | | --- | --- | | [AggParamType](./kibana-plugin-plugins-data-public.aggparamtype.md) | | -| [AggTypeFieldFilters](./kibana-plugin-plugins-data-public.aggtypefieldfilters.md) | A registry to store which are used to filter down available fields for a specific visualization and . | -| [AggTypeFilters](./kibana-plugin-plugins-data-public.aggtypefilters.md) | A registry to store which are used to filter down available aggregations for a specific visualization and . | | [Field](./kibana-plugin-plugins-data-public.field.md) | | | [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) | | | [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) | | @@ -53,7 +51,6 @@ | [AggParamOption](./kibana-plugin-plugins-data-public.aggparamoption.md) | | | [DataPublicPluginSetup](./kibana-plugin-plugins-data-public.datapublicpluginsetup.md) | | | [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | | -| [DateRangeKey](./kibana-plugin-plugins-data-public.daterangekey.md) | | | [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) | | | [FetchOptions](./kibana-plugin-plugins-data-public.fetchoptions.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-public.fieldformatconfig.md) | | @@ -75,7 +72,6 @@ | [ISearchStrategy](./kibana-plugin-plugins-data-public.isearchstrategy.md) | Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response. | | [ISyncSearchRequest](./kibana-plugin-plugins-data-public.isyncsearchrequest.md) | | | [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | | -| [OptionedParamEditorProps](./kibana-plugin-plugins-data-public.optionedparameditorprops.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | | | [Query](./kibana-plugin-plugins-data-public.query.md) | | | [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state | @@ -95,6 +91,7 @@ | Variable | Description | | --- | --- | +| [AggGroupLabels](./kibana-plugin-plugins-data-public.agggrouplabels.md) | | | [AggGroupNames](./kibana-plugin-plugins-data-public.agggroupnames.md) | | | [baseFormattersPublic](./kibana-plugin-plugins-data-public.baseformatterspublic.md) | | | [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-public.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | @@ -119,6 +116,7 @@ | Type Alias | Description | | --- | --- | | [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | | +| [AggGroupName](./kibana-plugin-plugins-data-public.agggroupname.md) | | | [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | | | [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | | | [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | | @@ -127,7 +125,6 @@ | [FieldFormatsContentType](./kibana-plugin-plugins-data-public.fieldformatscontenttype.md) | \* | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-public.fieldformatsgetconfigfn.md) | | | [IAggConfig](./kibana-plugin-plugins-data-public.iaggconfig.md) | AggConfig This class represents an aggregation, which is displayed in the left-hand nav of the Visualize app. | -| [IAggGroupNames](./kibana-plugin-plugins-data-public.iagggroupnames.md) | | | [IAggType](./kibana-plugin-plugins-data-public.iaggtype.md) | | | [IFieldFormat](./kibana-plugin-plugins-data-public.ifieldformat.md) | | | [IFieldFormatsRegistry](./kibana-plugin-plugins-data-public.ifieldformatsregistry.md) | | @@ -136,7 +133,6 @@ | [IndexPatternAggRestrictions](./kibana-plugin-plugins-data-public.indexpatternaggrestrictions.md) | | | [IndexPatternsContract](./kibana-plugin-plugins-data-public.indexpatternscontract.md) | | | [InputTimeRange](./kibana-plugin-plugins-data-public.inputtimerange.md) | | -| [IpRangeKey](./kibana-plugin-plugins-data-public.iprangekey.md) | | | [ISearch](./kibana-plugin-plugins-data-public.isearch.md) | | | [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | | [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | \* | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.aggparam.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.aggparam.md deleted file mode 100644 index 68e4371acc2f3..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.aggparam.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [OptionedParamEditorProps](./kibana-plugin-plugins-data-public.optionedparameditorprops.md) > [aggParam](./kibana-plugin-plugins-data-public.optionedparameditorprops.aggparam.md) - -## OptionedParamEditorProps.aggParam property - -Signature: - -```typescript -aggParam: { - options: T[]; - }; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.md deleted file mode 100644 index 00a440a0a775a..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.md +++ /dev/null @@ -1,18 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [OptionedParamEditorProps](./kibana-plugin-plugins-data-public.optionedparameditorprops.md) - -## OptionedParamEditorProps interface - -Signature: - -```typescript -export interface OptionedParamEditorProps -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [aggParam](./kibana-plugin-plugins-data-public.optionedparameditorprops.aggparam.md) | {
    options: T[];
    } | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 9a22339fd0530..67c4eac67a9e6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -9,12 +9,7 @@ ```typescript search: { aggs: { - AggConfigs: typeof AggConfigs; - aggGroupNamesMap: () => Record<"metrics" | "buckets", string>; - aggTypeFilters: import("./search/aggs/filter/agg_type_filters").AggTypeFilters; CidrMask: typeof CidrMask; - convertDateRangeToString: typeof convertDateRangeToString; - convertIPRangeToString: (range: import("./search").IpRangeKey, format: (val: any) => string) => string; dateHistogramInterval: typeof dateHistogramInterval; intervalOptions: ({ display: string; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index 271586bb8c582..3caba24748bfa 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -462,16 +462,6 @@ export const npStart = { types: aggTypesRegistry.start(), }, __LEGACY: { - AggConfig: sinon.fake(), - AggType: sinon.fake(), - aggTypeFieldFilters: { - addFilter: sinon.fake(), - filter: sinon.fake(), - }, - FieldParamType: sinon.fake(), - MetricAggType: sinon.fake(), - parentPipelineAggHelper: sinon.fake(), - siblingPipelineAggHelper: sinon.fake(), esClient: { search: sinon.fake(), msearch: sinon.fake(), diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 60d8079b22347..75deff23ce20d 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -288,13 +288,8 @@ export { import { // aggs - AggConfigs, - aggTypeFilters, - aggGroupNamesMap, CidrMask, - convertDateRangeToString, - convertIPRangeToString, - intervalOptions, // only used in Discover + intervalOptions, isDateHistogramBucketAggConfig, isNumberType, isStringType, @@ -326,26 +321,22 @@ export { ParsedInterval } from '../common'; export { // aggs + AggGroupLabels, + AggGroupName, AggGroupNames, - AggParam, // only the type is used externally, only in vis editor - AggParamOption, // only the type is used externally + AggParam, + AggParamOption, AggParamType, - AggTypeFieldFilters, // TODO convert to interface - AggTypeFilters, // TODO convert to interface AggConfigOptions, BUCKET_TYPES, - DateRangeKey, // only used in field formatter deserialization, which will live in data IAggConfig, IAggConfigs, - IAggGroupNames, IAggType, IFieldParamType, IMetricAggType, - IpRangeKey, // only used in field formatter deserialization, which will live in data METRIC_TYPES, - OptionedParamEditorProps, // only type is used externally OptionedParamType, - OptionedValueProp, // only type is used externally + OptionedValueProp, // search ES_SEARCH_STRATEGY, SYNC_SEARCH_STRATEGY, @@ -383,17 +374,12 @@ export { // Search namespace export const search = { aggs: { - AggConfigs, - aggGroupNamesMap, - aggTypeFilters, CidrMask, - convertDateRangeToString, - convertIPRangeToString, dateHistogramInterval, - intervalOptions, // only used in Discover + intervalOptions, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, - isDateHistogramBucketAggConfig, + isDateHistogramBucketAggConfig, // TODO: remove in build_pipeline refactor isNumberType, isStringType, isType, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 91dea66f06a94..b0bb911dca9e5 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -26,7 +26,6 @@ import { History } from 'history'; import { HttpSetup } from 'src/core/public'; import { HttpStart } from 'src/core/public'; import { IconType } from '@elastic/eui'; -import { IndexPatternField as IndexPatternField_2 } from 'src/plugins/data/public'; import { InjectedIntl } from '@kbn/i18n/react'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IUiSettingsClient } from 'src/core/public'; @@ -68,6 +67,20 @@ export type AggConfigOptions = Assign; +// Warning: (ae-missing-release-tag) "AggGroupLabels" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const AggGroupLabels: { + [AggGroupNames.Buckets]: string; + [AggGroupNames.Metrics]: string; + [AggGroupNames.None]: string; +}; + +// Warning: (ae-missing-release-tag) "AggGroupName" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AggGroupName = $Values; + // Warning: (ae-missing-release-tag) "AggGroupNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -108,32 +121,6 @@ export class AggParamType extends Ba makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; } -// Warning: (ae-missing-release-tag) "AggTypeFieldFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggTypeFieldFilter" -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggType" -// -// @public -export class AggTypeFieldFilters { - // Warning: (ae-forgotten-export) The symbol "AggTypeFieldFilter" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggTypeFieldFilter" - addFilter(filter: AggTypeFieldFilter): void; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "any" - filter(fields: IndexPatternField_2[], aggConfig: IAggConfig): IndexPatternField_2[]; - } - -// Warning: (ae-missing-release-tag) "AggTypeFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggTypeFilter" -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggConfig" -// -// @public -export class AggTypeFilters { - // Warning: (ae-forgotten-export) The symbol "AggTypeFilter" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggTypeFilter" - addFilter(filter: AggTypeFilter): void; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggType" - filter(aggTypes: IAggType[], indexPattern: IndexPattern, aggConfig: IAggConfig, aggFilter: string[]): IAggType[]; - } - // Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -266,16 +253,6 @@ export interface DataPublicPluginStart { }; } -// Warning: (ae-missing-release-tag) "DateRangeKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface DateRangeKey { - // (undocumented) - from: number; - // (undocumented) - to: number; -} - // @public (undocumented) export enum ES_FIELD_TYPES { // (undocumented) @@ -714,11 +691,6 @@ export type IAggConfig = AggConfig; // @internal export type IAggConfigs = AggConfigs; -// Warning: (ae-missing-release-tag) "IAggGroupNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type IAggGroupNames = $Values; - // Warning: (ae-forgotten-export) The symbol "AggType" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IAggType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1101,18 +1073,6 @@ export type InputTimeRange = TimeRange | { to: Moment; }; -// Warning: (ae-missing-release-tag) "IpRangeKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type IpRangeKey = { - type: 'mask'; - mask: string; -} | { - type: 'range'; - from: string; - to: string; -}; - // Warning: (ae-missing-release-tag) "IRequestTypesMap" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1290,16 +1250,6 @@ export enum METRIC_TYPES { TOP_HITS = "top_hits" } -// Warning: (ae-missing-release-tag) "OptionedParamEditorProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface OptionedParamEditorProps { - // (undocumented) - aggParam: { - options: T[]; - }; -} - // Warning: (ae-missing-release-tag) "OptionedParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1578,12 +1528,7 @@ export type SavedQueryTimeFilter = TimeRange & { // @public (undocumented) export const search: { aggs: { - AggConfigs: typeof AggConfigs; - aggGroupNamesMap: () => Record<"metrics" | "buckets", string>; - aggTypeFilters: import("./search/aggs/filter/agg_type_filters").AggTypeFilters; CidrMask: typeof CidrMask; - convertDateRangeToString: typeof convertDateRangeToString; - convertIPRangeToString: (range: import("./search").IpRangeKey, format: (val: any) => string) => string; dateHistogramInterval: typeof dateHistogramInterval; intervalOptions: ({ display: string; @@ -1870,21 +1815,20 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:384:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:377:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:378:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/aggs/agg_groups.ts b/src/plugins/data/public/search/aggs/agg_groups.ts index 9cebff76c9684..dec3397126e67 100644 --- a/src/plugins/data/public/search/aggs/agg_groups.ts +++ b/src/plugins/data/public/search/aggs/agg_groups.ts @@ -25,15 +25,17 @@ export const AggGroupNames = Object.freeze({ Metrics: 'metrics' as 'metrics', None: 'none' as 'none', }); -export type IAggGroupNames = $Values; -type IAggGroupNamesMap = () => Record<'buckets' | 'metrics', string>; +export type AggGroupName = $Values; -export const aggGroupNamesMap: IAggGroupNamesMap = () => ({ +export const AggGroupLabels = { + [AggGroupNames.Buckets]: i18n.translate('data.search.aggs.aggGroups.bucketsText', { + defaultMessage: 'Buckets', + }), [AggGroupNames.Metrics]: i18n.translate('data.search.aggs.aggGroups.metricsText', { defaultMessage: 'Metrics', }), - [AggGroupNames.Buckets]: i18n.translate('data.search.aggs.aggGroups.bucketsText', { - defaultMessage: 'Buckets', + [AggGroupNames.None]: i18n.translate('data.search.aggs.aggGroups.noneText', { + defaultMessage: 'None', }), -}); +}; diff --git a/src/plugins/data/public/search/aggs/filter/agg_type_filters.test.ts b/src/plugins/data/public/search/aggs/filter/agg_type_filters.test.ts deleted file mode 100644 index 58f5aef0b9dfd..0000000000000 --- a/src/plugins/data/public/search/aggs/filter/agg_type_filters.test.ts +++ /dev/null @@ -1,62 +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 { IndexPattern } from '../../../index_patterns'; -import { AggTypeFilters } from './agg_type_filters'; -import { IAggConfig, IAggType } from '../types'; - -describe('AggTypeFilters', () => { - let registry: AggTypeFilters; - const indexPattern = ({ id: '1234', fields: [], title: 'foo' } as unknown) as IndexPattern; - const aggConfig = {} as IAggConfig; - - beforeEach(() => { - registry = new AggTypeFilters(); - }); - - it('should filter nothing without registered filters', async () => { - const aggTypes = [{ name: 'count' }, { name: 'sum' }] as IAggType[]; - const filtered = registry.filter(aggTypes, indexPattern, aggConfig, []); - expect(filtered).toEqual(aggTypes); - }); - - it('should pass all aggTypes to the registered filter', async () => { - const aggTypes = [{ name: 'count' }, { name: 'sum' }] as IAggType[]; - const filter = jest.fn(); - registry.addFilter(filter); - registry.filter(aggTypes, indexPattern, aggConfig, []); - expect(filter).toHaveBeenCalledWith(aggTypes[0], indexPattern, aggConfig, []); - expect(filter).toHaveBeenCalledWith(aggTypes[1], indexPattern, aggConfig, []); - }); - - it('should allow registered filters to filter out aggTypes', async () => { - const aggTypes = [{ name: 'count' }, { name: 'sum' }, { name: 'avg' }] as IAggType[]; - let filtered = registry.filter(aggTypes, indexPattern, aggConfig, []); - expect(filtered).toEqual(aggTypes); - - registry.addFilter(() => true); - registry.addFilter(aggType => aggType.name !== 'count'); - filtered = registry.filter(aggTypes, indexPattern, aggConfig, []); - expect(filtered).toEqual([aggTypes[1], aggTypes[2]]); - - registry.addFilter(aggType => aggType.name !== 'avg'); - filtered = registry.filter(aggTypes, indexPattern, aggConfig, []); - expect(filtered).toEqual([aggTypes[1]]); - }); -}); diff --git a/src/plugins/data/public/search/aggs/filter/agg_type_filters.ts b/src/plugins/data/public/search/aggs/filter/agg_type_filters.ts deleted file mode 100644 index b8d192cd66b5a..0000000000000 --- a/src/plugins/data/public/search/aggs/filter/agg_type_filters.ts +++ /dev/null @@ -1,74 +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 { IndexPattern } from '../../../index_patterns'; -import { IAggConfig, IAggType } from '../types'; - -type AggTypeFilter = ( - aggType: IAggType, - indexPattern: IndexPattern, - aggConfig: IAggConfig, - aggFilter: string[] -) => boolean; - -/** - * A registry to store {@link AggTypeFilter} which are used to filter down - * available aggregations for a specific visualization and {@link AggConfig}. - */ -class AggTypeFilters { - private filters = new Set(); - - /** - * Register a new {@link AggTypeFilter} with this registry. - * - * @param filter The filter to register. - */ - public addFilter(filter: AggTypeFilter): void { - this.filters.add(filter); - } - - /** - * Returns the {@link AggType|aggTypes} filtered by all registered filters. - * - * @param aggTypes A list of aggTypes that will be filtered down by this registry. - * @param indexPattern The indexPattern for which this list should be filtered down. - * @param aggConfig The aggConfig for which the returning list will be used. - * @param schema - * @return A filtered list of the passed aggTypes. - */ - public filter( - aggTypes: IAggType[], - indexPattern: IndexPattern, - aggConfig: IAggConfig, - aggFilter: string[] - ) { - const allFilters = Array.from(this.filters); - const allowedAggTypes = aggTypes.filter(aggType => { - const isAggTypeAllowed = allFilters.every(filter => - filter(aggType, indexPattern, aggConfig, aggFilter) - ); - return isAggTypeAllowed; - }); - return allowedAggTypes; - } -} - -const aggTypeFilters = new AggTypeFilters(); - -export { aggTypeFilters, AggTypeFilters }; diff --git a/src/plugins/data/public/search/aggs/filter/index.ts b/src/plugins/data/public/search/aggs/filter/index.ts deleted file mode 100644 index 35d06807d0ec2..0000000000000 --- a/src/plugins/data/public/search/aggs/filter/index.ts +++ /dev/null @@ -1,21 +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. - */ - -export { aggTypeFilters, AggTypeFilters } from './agg_type_filters'; -export { propFilter } from './prop_filter'; diff --git a/src/plugins/data/public/search/aggs/index.ts b/src/plugins/data/public/search/aggs/index.ts index 5dfb6aeff8d14..1139d9c7ff722 100644 --- a/src/plugins/data/public/search/aggs/index.ts +++ b/src/plugins/data/public/search/aggs/index.ts @@ -24,7 +24,6 @@ export * from './agg_type'; export * from './agg_types'; export * from './agg_types_registry'; export * from './buckets'; -export * from './filter'; export * from './metrics'; export * from './param_types'; export * from './types'; diff --git a/src/plugins/data/public/search/aggs/param_types/field.ts b/src/plugins/data/public/search/aggs/param_types/field.ts index 4d67f41905c5a..63dbed9cec612 100644 --- a/src/plugins/data/public/search/aggs/param_types/field.ts +++ b/src/plugins/data/public/search/aggs/param_types/field.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { IAggConfig } from '../agg_config'; import { SavedObjectNotFound } from '../../../../../../plugins/kibana_utils/public'; import { BaseParamType } from './base'; -import { propFilter } from '../filter'; +import { propFilter } from '../utils'; import { isNestedField, KBN_FIELD_TYPES } from '../../../../common'; import { Field as IndexPatternField } from '../../../index_patterns'; import { GetInternalStartServicesFn } from '../../../types'; diff --git a/src/plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts b/src/plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts deleted file mode 100644 index f776a3deb23a1..0000000000000 --- a/src/plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts +++ /dev/null @@ -1,61 +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 { AggTypeFieldFilters } from './field_filters'; -import { IAggConfig } from '../../agg_config'; -import { Field as IndexPatternField } from '../../../../index_patterns'; - -describe('AggTypeFieldFilters', () => { - let registry: AggTypeFieldFilters; - const aggConfig = {} as IAggConfig; - - beforeEach(() => { - registry = new AggTypeFieldFilters(); - }); - - it('should filter nothing without registered filters', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexPatternField[]; - const filtered = registry.filter(fields, aggConfig); - expect(filtered).toEqual(fields); - }); - - it('should pass all fields to the registered filter', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexPatternField[]; - const filter = jest.fn(); - registry.addFilter(filter); - registry.filter(fields, aggConfig); - expect(filter).toHaveBeenCalledWith(fields[0], aggConfig); - expect(filter).toHaveBeenCalledWith(fields[1], aggConfig); - }); - - it('should allow registered filters to filter out fields', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexPatternField[]; - let filtered = registry.filter(fields, aggConfig); - expect(filtered).toEqual(fields); - - registry.addFilter(() => true); - registry.addFilter(field => field.name !== 'foo'); - filtered = registry.filter(fields, aggConfig); - expect(filtered).toEqual([fields[1]]); - - registry.addFilter(field => field.name !== 'bar'); - filtered = registry.filter(fields, aggConfig); - expect(filtered).toEqual([]); - }); -}); diff --git a/src/plugins/data/public/search/aggs/param_types/filter/field_filters.ts b/src/plugins/data/public/search/aggs/param_types/filter/field_filters.ts deleted file mode 100644 index 1cbf0c9ae3624..0000000000000 --- a/src/plugins/data/public/search/aggs/param_types/filter/field_filters.ts +++ /dev/null @@ -1,60 +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 { IndexPatternField } from 'src/plugins/data/public'; -import { IAggConfig } from '../../agg_config'; - -type AggTypeFieldFilter = (field: IndexPatternField, aggConfig: IAggConfig) => boolean; - -/** - * A registry to store {@link AggTypeFieldFilter} which are used to filter down - * available fields for a specific visualization and {@link AggType}. - */ -class AggTypeFieldFilters { - private filters = new Set(); - - /** - * Register a new {@link AggTypeFieldFilter} with this registry. - * This will be used by the {@link #filter|filter method}. - * - * @param filter The filter to register. - */ - public addFilter(filter: AggTypeFieldFilter): void { - this.filters.add(filter); - } - - /** - * Returns the {@link any|fields} filtered by all registered filters. - * - * @param fields An array of fields that will be filtered down by this registry. - * @param aggConfig The aggConfig for which the returning list will be used. - * @return A filtered list of the passed fields. - */ - public filter(fields: IndexPatternField[], aggConfig: IAggConfig) { - const allFilters = Array.from(this.filters); - const allowedAggTypeFields = fields.filter(field => { - const isAggTypeFieldAllowed = allFilters.every(filter => filter(field, aggConfig)); - return isAggTypeFieldAllowed; - }); - return allowedAggTypeFields; - } -} - -const aggTypeFieldFilters = new AggTypeFieldFilters(); - -export { aggTypeFieldFilters, AggTypeFieldFilters }; diff --git a/src/plugins/data/public/search/aggs/param_types/index.ts b/src/plugins/data/public/search/aggs/param_types/index.ts index c9e8a9879f427..e25dd55dbd3f2 100644 --- a/src/plugins/data/public/search/aggs/param_types/index.ts +++ b/src/plugins/data/public/search/aggs/param_types/index.ts @@ -20,7 +20,6 @@ export * from './agg'; export * from './base'; export * from './field'; -export * from './filter'; export * from './json'; export * from './optioned'; export * from './string'; diff --git a/src/plugins/data/public/search/aggs/param_types/optioned.ts b/src/plugins/data/public/search/aggs/param_types/optioned.ts index 9eb7ceda60711..45d0a65f69170 100644 --- a/src/plugins/data/public/search/aggs/param_types/optioned.ts +++ b/src/plugins/data/public/search/aggs/param_types/optioned.ts @@ -27,12 +27,6 @@ export interface OptionedValueProp { isCompatible: (agg: IAggConfig) => boolean; } -export interface OptionedParamEditorProps { - aggParam: { - options: T[]; - }; -} - export class OptionedParamType extends BaseParamType { options: OptionedValueProp[]; diff --git a/src/plugins/data/public/search/aggs/types.ts b/src/plugins/data/public/search/aggs/types.ts index 95a7a45013567..1c5b5b458ce90 100644 --- a/src/plugins/data/public/search/aggs/types.ts +++ b/src/plugins/data/public/search/aggs/types.ts @@ -19,20 +19,13 @@ import { IndexPattern } from '../../index_patterns'; import { - AggConfig, AggConfigSerialized, AggConfigs, AggParamsTerms, - AggType, - aggTypeFieldFilters, AggTypesRegistrySetup, AggTypesRegistryStart, CreateAggConfigParams, - FieldParamType, getCalculateAutoTimeExpression, - MetricAggType, - parentPipelineAggHelper, - siblingPipelineAggHelper, } from './'; export { IAggConfig, AggConfigSerialized } from './agg_config'; @@ -43,7 +36,7 @@ export { IFieldParamType } from './param_types'; export { IMetricAggType } from './metrics/metric_agg_type'; export { DateRangeKey } from './buckets/lib/date_range'; export { IpRangeKey } from './buckets/lib/ip_range'; -export { OptionedValueProp, OptionedParamEditorProps } from './param_types/optioned'; +export { OptionedValueProp } from './param_types/optioned'; /** @internal */ export interface SearchAggsSetup { @@ -51,17 +44,6 @@ export interface SearchAggsSetup { types: AggTypesRegistrySetup; } -/** @internal */ -export interface SearchAggsStartLegacy { - AggConfig: typeof AggConfig; - AggType: typeof AggType; - aggTypeFieldFilters: typeof aggTypeFieldFilters; - FieldParamType: typeof FieldParamType; - MetricAggType: typeof MetricAggType; - parentPipelineAggHelper: typeof parentPipelineAggHelper; - siblingPipelineAggHelper: typeof siblingPipelineAggHelper; -} - /** @internal */ export interface SearchAggsStart { calculateAutoTimeExpression: ReturnType; diff --git a/src/plugins/data/public/search/aggs/utils/index.ts b/src/plugins/data/public/search/aggs/utils/index.ts index 23606bd109342..169d872b17d3a 100644 --- a/src/plugins/data/public/search/aggs/utils/index.ts +++ b/src/plugins/data/public/search/aggs/utils/index.ts @@ -18,4 +18,5 @@ */ export * from './calculate_auto_time_expression'; +export * from './prop_filter'; export * from './to_angular_json'; diff --git a/src/plugins/data/public/search/aggs/filter/prop_filter.test.ts b/src/plugins/data/public/search/aggs/utils/prop_filter.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/filter/prop_filter.test.ts rename to src/plugins/data/public/search/aggs/utils/prop_filter.test.ts diff --git a/src/plugins/data/public/search/aggs/filter/prop_filter.ts b/src/plugins/data/public/search/aggs/utils/prop_filter.ts similarity index 97% rename from src/plugins/data/public/search/aggs/filter/prop_filter.ts rename to src/plugins/data/public/search/aggs/utils/prop_filter.ts index e6b5f3831e65d..01e98a68d3949 100644 --- a/src/plugins/data/public/search/aggs/filter/prop_filter.ts +++ b/src/plugins/data/public/search/aggs/utils/prop_filter.ts @@ -28,7 +28,7 @@ type FilterFunc

    = (item: T[P]) => boolean; * * @returns the filter function which can be registered with angular */ -function propFilter

    (prop: P) { +export function propFilter

    (prop: P) { /** * List filtering function which accepts an array or list of values that a property * must contain @@ -92,5 +92,3 @@ function propFilter

    (prop: P) { }); }; } - -export { propFilter }; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index dd196074c8173..44082040b5b0b 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -18,7 +18,6 @@ */ import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; -import { AggTypeFieldFilters } from './aggs/param_types/filter'; import { ISearchStart } from './types'; import { searchSourceMock, createSearchSourceMock } from './search_source/mocks'; @@ -34,13 +33,6 @@ const searchStartMock: jest.Mocked = { search: jest.fn(), searchSource: searchSourceMock, __LEGACY: { - AggConfig: jest.fn() as any, - AggType: jest.fn(), - aggTypeFieldFilters: new AggTypeFieldFilters(), - FieldParamType: jest.fn(), - MetricAggType: jest.fn(), - parentPipelineAggHelper: jest.fn() as any, - siblingPipelineAggHelper: jest.fn() as any, esClient: { search: jest.fn(), msearch: jest.fn(), diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index b59524baa9fa7..4d183797dfef0 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -39,16 +39,9 @@ import { SearchInterceptor } from './search_interceptor'; import { getAggTypes, getAggTypesFunctions, - AggType, AggTypesRegistry, - AggConfig, AggConfigs, - FieldParamType, getCalculateAutoTimeExpression, - MetricAggType, - aggTypeFieldFilters, - parentPipelineAggHelper, - siblingPipelineAggHelper, } from './aggs'; import { FieldFormatsStart } from '../field_formats'; import { ISearchGeneric } from './i_search'; @@ -156,13 +149,6 @@ export class SearchService implements Plugin { const legacySearch = { esClient: this.esClient!, - AggConfig, - AggType, - aggTypeFieldFilters, - FieldParamType, - MetricAggType, - parentPipelineAggHelper, - siblingPipelineAggHelper, }; const searchSourceDependencies: SearchSourceDependencies = { diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 99d111ce1574e..1687c8f983393 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -18,7 +18,7 @@ */ import { CoreStart, SavedObjectReference } from 'kibana/public'; -import { SearchAggsSetup, SearchAggsStart, SearchAggsStartLegacy } from './aggs'; +import { SearchAggsSetup, SearchAggsStart } from './aggs'; import { ISearch, ISearchGeneric } from './i_search'; import { TStrategyTypes } from './strategy_types'; import { LegacyApiCaller } from './legacy/es_client'; @@ -88,5 +88,5 @@ export interface ISearchStart { references: SavedObjectReference[] ) => Promise; }; - __LEGACY: ISearchStartLegacy & SearchAggsStartLegacy; + __LEGACY: ISearchStartLegacy; } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index fb3d6efa63826..1860a4625afc0 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -260,19 +260,10 @@ exports[`SavedObjectsTable import should show the flyout 1`] = ` search={ Object { "__LEGACY": Object { - "AggConfig": [MockFunction], - "AggType": [MockFunction], - "FieldParamType": [MockFunction], - "MetricAggType": [MockFunction], - "aggTypeFieldFilters": AggTypeFieldFilters { - "filters": Set {}, - }, "esClient": Object { "msearch": [MockFunction], "search": [MockFunction], }, - "parentPipelineAggHelper": [MockFunction], - "siblingPipelineAggHelper": [MockFunction], }, "aggs": Object { "calculateAutoTimeExpression": [Function], diff --git a/src/plugins/vis_default_editor/public/agg_filters/agg_type_field_filters.ts b/src/plugins/vis_default_editor/public/agg_filters/agg_type_field_filters.ts new file mode 100644 index 0000000000000..15df2f0acccd1 --- /dev/null +++ b/src/plugins/vis_default_editor/public/agg_filters/agg_type_field_filters.ts @@ -0,0 +1,49 @@ +/* + * 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 { IAggConfig, IndexPatternField } from '../../../data/public'; + +type AggTypeFieldFilter = (field: IndexPatternField, aggConfig: IAggConfig) => boolean; + +const filters: AggTypeFieldFilter[] = [ + /** + * Check index pattern aggregation restrictions + * and limit available fields for a given aggType based on that. + */ + (field, aggConfig) => { + const indexPattern = aggConfig.getIndexPattern(); + const aggRestrictions = indexPattern.getAggregationRestrictions(); + + if (!aggRestrictions) { + return true; + } + + const aggName = aggConfig.type && aggConfig.type.name; + const aggFields = aggRestrictions[aggName]; + return !!aggFields && !!aggFields[field.name]; + }, +]; + +export function filterAggTypeFields(fields: IndexPatternField[], aggConfig: IAggConfig) { + const allowedAggTypeFields = fields.filter(field => { + const isAggTypeFieldAllowed = filters.every(filter => filter(field, aggConfig)); + return isAggTypeFieldAllowed; + }); + return allowedAggTypeFields; +} diff --git a/src/plugins/vis_default_editor/public/agg_filters/agg_type_filters.ts b/src/plugins/vis_default_editor/public/agg_filters/agg_type_filters.ts new file mode 100644 index 0000000000000..2cf1acba4d228 --- /dev/null +++ b/src/plugins/vis_default_editor/public/agg_filters/agg_type_filters.ts @@ -0,0 +1,75 @@ +/* + * 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 { IAggType, IAggConfig, IndexPattern, search } from '../../../data/public'; + +const { propFilter } = search.aggs; +const filterByName = propFilter('name'); + +type AggTypeFilter = ( + aggType: IAggType, + indexPattern: IndexPattern, + aggConfig: IAggConfig, + aggFilter: string[] +) => boolean; + +const filters: AggTypeFilter[] = [ + /** + * This filter checks the defined aggFilter in the schemas of that visualization + * and limits available aggregations based on that. + */ + (aggType, indexPattern, aggConfig, aggFilter) => { + const doesSchemaAllowAggType = filterByName([aggType], aggFilter).length !== 0; + return doesSchemaAllowAggType; + }, + /** + * Check index pattern aggregation restrictions and limit available aggTypes. + */ + (aggType, indexPattern, aggConfig, aggFilter) => { + const aggRestrictions = indexPattern.getAggregationRestrictions(); + + if (!aggRestrictions) { + return true; + } + + const aggName = aggType.name; + // Only return agg types which are specified in the agg restrictions, + // except for `count` which should always be returned. + return ( + aggName === 'count' || + (!!aggRestrictions && Object.keys(aggRestrictions).includes(aggName)) || + false + ); + }, +]; + +export function filterAggTypes( + aggTypes: IAggType[], + indexPattern: IndexPattern, + aggConfig: IAggConfig, + aggFilter: string[] +) { + const allowedAggTypes = aggTypes.filter(aggType => { + const isAggTypeAllowed = filters.every(filter => + filter(aggType, indexPattern, aggConfig, aggFilter) + ); + return isAggTypeAllowed; + }); + return allowedAggTypes; +} diff --git a/src/plugins/data/public/search/aggs/param_types/filter/index.ts b/src/plugins/vis_default_editor/public/agg_filters/index.ts similarity index 91% rename from src/plugins/data/public/search/aggs/param_types/filter/index.ts rename to src/plugins/vis_default_editor/public/agg_filters/index.ts index 2e0039c96a192..2b08449fb3161 100644 --- a/src/plugins/data/public/search/aggs/param_types/filter/index.ts +++ b/src/plugins/vis_default_editor/public/agg_filters/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { aggTypeFieldFilters, AggTypeFieldFilters } from './field_filters'; +export * from './agg_type_filters'; +export * from './agg_type_field_filters'; diff --git a/src/plugins/vis_default_editor/public/components/agg_common_props.ts b/src/plugins/vis_default_editor/public/components/agg_common_props.ts index 0c130a96230b4..40d7b79bfbefc 100644 --- a/src/plugins/vis_default_editor/public/components/agg_common_props.ts +++ b/src/plugins/vis_default_editor/public/components/agg_common_props.ts @@ -18,7 +18,7 @@ */ import { VisParams } from 'src/plugins/visualizations/public'; -import { IAggType, IAggConfig, IAggGroupNames } from 'src/plugins/data/public'; +import { IAggType, IAggConfig, AggGroupName } from 'src/plugins/data/public'; import { Schema } from '../schemas'; import { EditorVisState } from './sidebar/state/reducers'; @@ -30,7 +30,7 @@ export type ReorderAggs = (sourceAgg: IAggConfig, destinationAgg: IAggConfig) => export interface DefaultEditorCommonProps { formIsTouched: boolean; - groupName: IAggGroupNames; + groupName: AggGroupName; metricAggs: IAggConfig[]; state: EditorVisState; setAggParamValue: ( diff --git a/src/plugins/vis_default_editor/public/components/agg_group.tsx b/src/plugins/vis_default_editor/public/components/agg_group.tsx index 72515d0845926..fae9de6959ef1 100644 --- a/src/plugins/vis_default_editor/public/components/agg_group.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_group.tsx @@ -30,7 +30,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AggGroupNames, search, IAggConfig, TimeRange } from '../../../data/public'; +import { AggGroupNames, AggGroupLabels, IAggConfig, TimeRange } from '../../../data/public'; import { DefaultEditorAgg } from './agg'; import { DefaultEditorAggAdd } from './agg_add'; import { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from './agg_common_props'; @@ -70,7 +70,7 @@ function DefaultEditorAggGroup({ setValidity, timeRange, }: DefaultEditorAggGroupProps) { - const groupNameLabel = (search.aggs.aggGroupNamesMap() as any)[groupName]; + const groupNameLabel = AggGroupLabels[groupName]; // e.g. buckets can have no aggs const schemaNames = schemas.map(s => s.name); const group: IAggConfig[] = useMemo( diff --git a/src/plugins/vis_default_editor/public/components/agg_param_props.ts b/src/plugins/vis_default_editor/public/components/agg_param_props.ts index aec332e8674d7..076bddc9551ea 100644 --- a/src/plugins/vis_default_editor/public/components/agg_param_props.ts +++ b/src/plugins/vis_default_editor/public/components/agg_param_props.ts @@ -17,7 +17,12 @@ * under the License. */ -import { IAggConfig, AggParam, IndexPatternField } from 'src/plugins/data/public'; +import { + IAggConfig, + AggParam, + IndexPatternField, + OptionedValueProp, +} from 'src/plugins/data/public'; import { ComboBoxGroupedOptions } from '../utils'; import { EditorConfig } from './utils'; import { Schema } from '../schemas'; @@ -46,3 +51,9 @@ export interface AggParamEditorProps extends AggParamCommonProp setValidity(isValid: boolean): void; setTouched(): void; } + +export interface OptionedParamEditorProps { + aggParam: { + options: T[]; + }; +} diff --git a/src/plugins/vis_default_editor/public/components/agg_params.tsx b/src/plugins/vis_default_editor/public/components/agg_params.tsx index 3674e39b558d2..d36c2d0e7625b 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_params.tsx @@ -112,20 +112,8 @@ function DefaultEditorAggParams({ fieldName, ]); const params = useMemo( - () => - getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas, hideCustomLabel }, - services.data.search.__LEGACY.aggTypeFieldFilters - ), - [ - agg, - editorConfig, - metricAggs, - state, - schemas, - hideCustomLabel, - services.data.search.__LEGACY.aggTypeFieldFilters, - ] + () => getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas, hideCustomLabel }), + [agg, editorConfig, metricAggs, state, schemas, hideCustomLabel] ); const allParams = [...params.basic, ...params.advanced]; const [paramsState, onChangeParamsState] = useReducer( diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts index bed2561341737..834ad8b70ad0d 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts @@ -23,7 +23,6 @@ import { IAggConfig, IAggType, IndexPattern, - IndexPatternField, } from 'src/plugins/data/public'; import { getAggParamsToRender, @@ -39,12 +38,6 @@ jest.mock('../utils', () => ({ groupAndSortBy: jest.fn(() => ['indexedFields']), })); -const mockFilter: any = { - filter(fields: IndexPatternField[]): IndexPatternField[] { - return fields; - }, -}; - describe('DefaultEditorAggParams helpers', () => { describe('getAggParamsToRender', () => { let agg: IAggConfig; @@ -72,20 +65,14 @@ describe('DefaultEditorAggParams helpers', () => { }, schema: 'metric', } as IAggConfig; - const params = getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas }, - mockFilter - ); + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); expect(params).toEqual(emptyParams); }); it('should not create any param if there is no agg type', () => { agg = { schema: 'metric' } as IAggConfig; - const params = getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas }, - mockFilter - ); + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); expect(params).toEqual(emptyParams); }); @@ -101,10 +88,7 @@ describe('DefaultEditorAggParams helpers', () => { hidden: true, }, }; - const params = getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas }, - mockFilter - ); + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); expect(params).toEqual(emptyParams); }); @@ -116,10 +100,7 @@ describe('DefaultEditorAggParams helpers', () => { }, schema: 'metric2', } as any) as IAggConfig; - const params = getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas }, - mockFilter - ); + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); expect(params).toEqual(emptyParams); }); @@ -152,16 +133,14 @@ describe('DefaultEditorAggParams helpers', () => { { name: '@timestamp', type: 'date' }, { name: 'geo_desc', type: 'string' }, ], + getAggregationRestrictions: jest.fn(), })), params: { orderBy: 'orderBy', field: 'field', }, } as any) as IAggConfig; - const params = getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas }, - mockFilter - ); + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); expect(params).toEqual({ basic: [ @@ -190,7 +169,6 @@ describe('DefaultEditorAggParams helpers', () => { ], advanced: [], }); - expect(agg.getIndexPattern).toBeCalledTimes(1); }); }); diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts index a32bd76bafa5a..9977f1e5e71fc 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -20,7 +20,6 @@ import { get, isEmpty } from 'lodash'; import { - AggTypeFieldFilters, IAggConfig, AggParam, IFieldParamType, @@ -28,13 +27,13 @@ import { IndexPattern, IndexPatternField, } from 'src/plugins/data/public'; +import { filterAggTypes, filterAggTypeFields } from '../agg_filters'; import { groupAndSortBy, ComboBoxGroupedOptions } from '../utils'; import { AggTypeState, AggParamsState } from './agg_params_state'; import { AggParamEditorProps } from './agg_param_props'; import { aggParamsMap } from './agg_params_map'; import { EditorConfig } from './utils'; import { Schema, getSchemaByName } from '../schemas'; -import { search } from '../../../data/public'; import { EditorVisState } from './sidebar/state/reducers'; interface ParamInstanceBase { @@ -53,10 +52,14 @@ export interface ParamInstance extends ParamInstanceBase { value: unknown; } -function getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas, hideCustomLabel }: ParamInstanceBase, - aggTypeFieldFilters: AggTypeFieldFilters -) { +function getAggParamsToRender({ + agg, + editorConfig, + metricAggs, + state, + schemas, + hideCustomLabel, +}: ParamInstanceBase) { const params = { basic: [] as ParamInstance[], advanced: [] as ParamInstance[], @@ -89,7 +92,7 @@ function getAggParamsToRender( availableFields = availableFields.filter(field => field.type === 'number'); } } - fields = aggTypeFieldFilters.filter(availableFields, agg); + fields = filterAggTypeFields(availableFields, agg); indexedFields = groupAndSortBy(fields, 'type', 'name'); if (fields && !indexedFields.length && index > 0) { @@ -138,12 +141,7 @@ function getAggTypeOptions( groupName: string, allowedAggs: string[] ): ComboBoxGroupedOptions { - const aggTypeOptions = search.aggs.aggTypeFilters.filter( - aggTypes[groupName], - indexPattern, - agg, - allowedAggs - ); + const aggTypeOptions = filterAggTypes(aggTypes[groupName], indexPattern, agg, allowedAggs); return groupAndSortBy(aggTypeOptions as any[], 'subtype', 'title'); } diff --git a/src/plugins/vis_default_editor/public/components/controls/order.tsx b/src/plugins/vis_default_editor/public/components/controls/order.tsx index e609bf9adf790..3c0224564300a 100644 --- a/src/plugins/vis_default_editor/public/components/controls/order.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order.tsx @@ -21,8 +21,8 @@ import React, { useEffect } from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { OptionedValueProp, OptionedParamEditorProps } from 'src/plugins/data/public'; -import { AggParamEditorProps } from '../agg_param_props'; +import { OptionedValueProp } from 'src/plugins/data/public'; +import { AggParamEditorProps, OptionedParamEditorProps } from '../agg_param_props'; function OrderParamEditor({ aggParam, diff --git a/src/plugins/vis_default_editor/public/components/controls/top_aggregate.tsx b/src/plugins/vis_default_editor/public/components/controls/top_aggregate.tsx index ad23cec87800f..66abb88b97d29 100644 --- a/src/plugins/vis_default_editor/public/components/controls/top_aggregate.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/top_aggregate.tsx @@ -26,10 +26,9 @@ import { IAggConfig, AggParam, OptionedValueProp, - OptionedParamEditorProps, OptionedParamType, } from 'src/plugins/data/public'; -import { AggParamEditorProps } from '../agg_param_props'; +import { AggParamEditorProps, OptionedParamEditorProps } from '../agg_param_props'; export interface AggregateValueProp extends OptionedValueProp { isCompatible(aggConfig: IAggConfig): boolean; diff --git a/src/plugins/vis_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx index 1c2ddbc314f99..8088921ba7fda 100644 --- a/src/plugins/vis_default_editor/public/default_editor.tsx +++ b/src/plugins/vis_default_editor/public/default_editor.tsx @@ -22,7 +22,6 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EditorRenderProps } from 'src/plugins/visualize/public'; import { PanelsContainer, Panel } from '../../kibana_react/public'; -import './vis_type_agg_filter'; import { DefaultEditorSideBar } from './components/sidebar'; import { DefaultEditorControllerState } from './default_editor_controller'; import { getInitialWidth } from './editor_size'; diff --git a/src/plugins/vis_default_editor/public/schemas.ts b/src/plugins/vis_default_editor/public/schemas.ts index 05ba5fa9c9419..26d1cbe91b996 100644 --- a/src/plugins/vis_default_editor/public/schemas.ts +++ b/src/plugins/vis_default_editor/public/schemas.ts @@ -21,7 +21,7 @@ import _, { defaults } from 'lodash'; import { Optional } from '@kbn/utility-types'; -import { AggGroupNames, AggParam, IAggGroupNames } from '../../data/public'; +import { AggGroupNames, AggParam, AggGroupName } from '../../data/public'; export interface ISchemas { [AggGroupNames.Buckets]: Schema[]; @@ -32,7 +32,7 @@ export interface ISchemas { export interface Schema { aggFilter: string[]; editor: boolean | string; - group: IAggGroupNames; + group: AggGroupName; max: number; min: number; name: string; diff --git a/src/plugins/vis_default_editor/public/vis_type_agg_filter.ts b/src/plugins/vis_default_editor/public/vis_type_agg_filter.ts deleted file mode 100644 index bf5661f42a9f5..0000000000000 --- a/src/plugins/vis_default_editor/public/vis_type_agg_filter.ts +++ /dev/null @@ -1,33 +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 { IAggType, IAggConfig, IndexPattern, search } from '../../data/public'; - -const { aggTypeFilters, propFilter } = search.aggs; -const filterByName = propFilter('name'); - -/** - * This filter checks the defined aggFilter in the schemas of that visualization - * and limits available aggregations based on that. - */ -aggTypeFilters.addFilter( - (aggType: IAggType, indexPatterns: IndexPattern, aggConfig: IAggConfig, aggFilter: string[]) => { - const doesSchemaAllowAggType = filterByName([aggType], aggFilter).length !== 0; - return doesSchemaAllowAggType; - } -); diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json index 4c7dcb48a4d3f..f897051d3ed8a 100644 --- a/x-pack/plugins/rollup/kibana.json +++ b/x-pack/plugins/rollup/kibana.json @@ -7,8 +7,7 @@ "requiredPlugins": [ "indexPatternManagement", "management", - "licensing", - "data" + "licensing" ], "optionalPlugins": [ "home", diff --git a/x-pack/plugins/rollup/public/plugin.ts b/x-pack/plugins/rollup/public/plugin.ts index fd1b90fbc9855..5bb678ac35d06 100644 --- a/x-pack/plugins/rollup/public/plugin.ts +++ b/x-pack/plugins/rollup/public/plugin.ts @@ -11,10 +11,6 @@ import { rollupBadgeExtension, rollupToggleExtension } from './extend_index_mana import { RollupIndexPatternCreationConfig } from './index_pattern_creation/rollup_index_pattern_creation_config'; // @ts-ignore import { RollupIndexPatternListConfig } from './index_pattern_list/rollup_index_pattern_list_config'; -// @ts-ignore -import { initAggTypeFilter } from './visualize/agg_type_filter'; -// @ts-ignore -import { initAggTypeFieldFilter } from './visualize/agg_type_field_filter'; import { CONFIG_ROLLUPS, UIM_APP_NAME } from '../common'; import { FeatureCatalogueCategory, @@ -25,7 +21,6 @@ import { CRUD_APP_BASE_PATH } from './crud_app/constants'; import { ManagementSetup } from '../../../../src/plugins/management/public'; import { IndexManagementPluginSetup } from '../../index_management/public'; import { IndexPatternManagementSetup } from '../../../../src/plugins/index_pattern_management/public'; -import { DataPublicPluginStart, search } from '../../../../src/plugins/data/public'; // @ts-ignore import { setEsBaseAndXPackBase, setHttp } from './crud_app/services/index'; import { setNotifications, setFatalErrors, setUiStatsReporter } from './kibana_services'; @@ -39,10 +34,6 @@ export interface RollupPluginSetupDependencies { usageCollection?: UsageCollectionSetup; } -export interface RollupPluginStartDependencies { - data: DataPublicPluginStart; -} - export class RollupPlugin implements Plugin { setup( core: CoreSetup, @@ -108,16 +99,9 @@ export class RollupPlugin implements Plugin { } } - start(core: CoreStart, plugins: RollupPluginStartDependencies) { + start(core: CoreStart) { setHttp(core.http); setNotifications(core.notifications); setEsBaseAndXPackBase(core.docLinks.ELASTIC_WEBSITE_URL, core.docLinks.DOC_LINK_VERSION); - - const isRollupIndexPatternsEnabled = core.uiSettings.get(CONFIG_ROLLUPS); - - if (isRollupIndexPatternsEnabled) { - initAggTypeFilter(search.aggs.aggTypeFilters); - initAggTypeFieldFilter(plugins.data.search.__LEGACY.aggTypeFieldFilters); - } } } diff --git a/x-pack/plugins/rollup/public/visualize/agg_type_field_filter.js b/x-pack/plugins/rollup/public/visualize/agg_type_field_filter.js deleted file mode 100644 index 6f44e0ef90efd..0000000000000 --- a/x-pack/plugins/rollup/public/visualize/agg_type_field_filter.js +++ /dev/null @@ -1,22 +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. - */ - -export function initAggTypeFieldFilter(aggTypeFieldFilters) { - /** - * If rollup index pattern, check its capabilities - * and limit available fields for a given aggType based on that. - */ - aggTypeFieldFilters.addFilter((field, aggConfig) => { - const indexPattern = aggConfig.getIndexPattern(); - if (!indexPattern || indexPattern.type !== 'rollup') { - return true; - } - const aggName = aggConfig.type && aggConfig.type.name; - const aggFields = - indexPattern.typeMeta && indexPattern.typeMeta.aggs && indexPattern.typeMeta.aggs[aggName]; - return aggFields && aggFields[field.name]; - }); -} diff --git a/x-pack/plugins/rollup/public/visualize/agg_type_filter.js b/x-pack/plugins/rollup/public/visualize/agg_type_filter.js deleted file mode 100644 index 5f9fab3061a19..0000000000000 --- a/x-pack/plugins/rollup/public/visualize/agg_type_filter.js +++ /dev/null @@ -1,23 +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. - */ - -export function initAggTypeFilter(aggTypeFilters) { - /** - * If rollup index pattern, check its capabilities - * and limit available aggregations based on that. - */ - aggTypeFilters.addFilter((aggType, indexPattern) => { - if (indexPattern.type !== 'rollup') { - return true; - } - const aggName = aggType.name; - const aggs = indexPattern.typeMeta && indexPattern.typeMeta.aggs; - - // Return doc_count (which is collected by default for rollup date histogram, histogram, and terms) - // and the rest of the defined metrics from capabilities. - return aggName === 'count' || Object.keys(aggs).includes(aggName); - }); -} From f4db1c2b92841f36d87643d9a80754d1b7f66895 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 30 Apr 2020 21:26:08 -0700 Subject: [PATCH 062/122] Fixed alert Edit flyout shows the error message when one of this actions has a preconfigured action type (#64742) * Fixed alert Edit flyout shows the error message when one of this actions has a preconfigured action type * Added tests * fixed config * fixed tests * Fixed browser error about memory * Fixed type check * Fixed func tests * fixed more tests * fixed tests --- .../action_connector_form/action_form.tsx | 28 +++++++-- .../components/alert_instances_route.tsx | 3 +- .../sections/alert_form/alert_edit.tsx | 1 + .../apps/triggers_actions_ui/connectors.ts | 59 +++++++++--------- .../apps/triggers_actions_ui/details.ts | 60 ++++++++++++------- .../apps/triggers_actions_ui/home_page.ts | 8 ++- x-pack/test/functional_with_es_ssl/config.ts | 23 ++++++- 7 files changed, 121 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 0027837c913d1..531e9e1926ff4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -106,10 +106,6 @@ export const ActionForm = ({ index[actionTypeItem.id] = actionTypeItem; } setActionTypesIndex(index); - const hasActionsDisabled = actions.some(action => !index[action.actionTypeId].enabled); - if (setHasActionsDisabled) { - setHasActionsDisabled(hasActionsDisabled); - } } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -129,7 +125,8 @@ export const ActionForm = ({ (async () => { try { setIsLoadingConnectors(true); - setConnectors(await loadConnectors({ http })); + const loadedConnectors = await loadConnectors({ http }); + setConnectors(loadedConnectors); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -146,6 +143,27 @@ export const ActionForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + const setActionTypesAvalilability = () => { + const preconfiguredConnectors = connectors.filter(connector => connector.isPreconfigured); + const hasActionsDisabled = actions.some( + action => + !actionTypesIndex![action.actionTypeId].enabled && + !checkActionFormActionTypeEnabled( + actionTypesIndex![action.actionTypeId], + preconfiguredConnectors + ).isEnabled + ); + if (setHasActionsDisabled) { + setHasActionsDisabled(hasActionsDisabled); + } + }; + if (connectors.length > 0 && actionTypesIndex) { + setActionTypesAvalilability(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connectors, actionTypesIndex]); + const preconfiguredMessage = i18n.translate( 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx index b9d08abae1684..a02b44523e26c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx @@ -32,7 +32,8 @@ export const AlertInstancesRoute: React.FunctionComponent = useEffect(() => { getAlertState(alert.id, loadAlertState, setAlertState, toastNotifications); - }, [alert, loadAlertState, toastNotifications]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [alert]); return alertState ? ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index c9cf59d87414f..00bc9874face1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -150,6 +150,7 @@ export const AlertEdit = ({ size="s" color="danger" iconType="alert" + data-test-subj="hasActionsDisabled" title={i18n.translate( 'xpack.triggersActionsUI.sections.alertEdit.disabledActionsWarningTitle', { defaultMessage: 'This alert has actions that are disabled' } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index 562f64656319e..1facc05bc186d 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -21,10 +21,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Connectors', function() { before(async () => { await alerting.actions.createAction({ - name: `server-log-${Date.now()}`, - actionTypeId: '.server-log', + name: `slack-${Date.now()}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }); await pageObjects.common.navigateToApp('triggersActions'); @@ -36,12 +38,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.clickCreateConnectorButton(); - await testSubjects.click('.server-log-card'); + await testSubjects.click('.slack-card'); + + await testSubjects.setValue('nameInput', connectorName); - const nameInput = await testSubjects.find('nameInput'); - await nameInput.click(); - await nameInput.clearValue(); - await nameInput.type(connectorName); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); @@ -54,7 +55,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(searchResults).to.eql([ { name: connectorName, - actionType: 'Server log', + actionType: 'Slack', referencedByCount: '0', }, ]); @@ -66,12 +67,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.clickCreateConnectorButton(); - await testSubjects.click('.server-log-card'); + await testSubjects.click('.slack-card'); + + await testSubjects.setValue('nameInput', connectorName); - const nameInput = await testSubjects.find('nameInput'); - await nameInput.click(); - await nameInput.clearValue(); - await nameInput.type(connectorName); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); @@ -84,10 +84,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); - const nameInputToUpdate = await testSubjects.find('nameInput'); - await nameInputToUpdate.click(); - await nameInputToUpdate.clearValue(); - await nameInputToUpdate.type(updatedConnectorName); + await testSubjects.setValue('nameInput', updatedConnectorName); + + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); await find.clickByCssSelector('[data-test-subj="saveEditedActionButton"]:not(disabled)'); @@ -100,7 +99,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(searchResultsAfterEdit).to.eql([ { name: updatedConnectorName, - actionType: 'Server log', + actionType: 'Slack', referencedByCount: '0', }, ]); @@ -110,12 +109,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function createConnector(connectorName: string) { await pageObjects.triggersActionsUI.clickCreateConnectorButton(); - await testSubjects.click('.server-log-card'); + await testSubjects.click('.slack-card'); + + await testSubjects.setValue('nameInput', connectorName); - const nameInput = await testSubjects.find('nameInput'); - await nameInput.click(); - await nameInput.clearValue(); - await nameInput.type(connectorName); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); await pageObjects.common.closeToast(); @@ -148,12 +146,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function createConnector(connectorName: string) { await pageObjects.triggersActionsUI.clickCreateConnectorButton(); - await testSubjects.click('.server-log-card'); + await testSubjects.click('.slack-card'); + + await testSubjects.setValue('nameInput', connectorName); - const nameInput = await testSubjects.find('nameInput'); - await nameInput.click(); - await nameInput.clearValue(); - await nameInput.type(connectorName); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); await pageObjects.common.closeToast(); @@ -186,7 +183,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should not be able to delete a preconfigured connector', async () => { - const preconfiguredConnectorName = 'xyz'; + const preconfiguredConnectorName = 'Serverlog'; await pageObjects.triggersActionsUI.searchConnectors(preconfiguredConnectorName); const searchResults = await pageObjects.triggersActionsUI.getConnectorsList(); @@ -197,7 +194,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should not be able to edit a preconfigured connector', async () => { - const preconfiguredConnectorName = 'xyz'; + const preconfiguredConnectorName = 'xyztest'; await pageObjects.triggersActionsUI.searchConnectors(preconfiguredConnectorName); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 6ff065c1f4ab2..7970c9b24427e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -27,16 +27,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const actions = await Promise.all([ alerting.actions.createAction({ - name: `server-log-${testRunUuid}-${0}`, - actionTypeId: '.server-log', + name: `slack-${testRunUuid}-${0}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }), alerting.actions.createAction({ - name: `server-log-${testRunUuid}-${1}`, - actionTypeId: '.server-log', + name: `slack-${testRunUuid}-${1}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }), ]); @@ -72,7 +76,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(alertType).to.be(`Always Firing`); const { actionType, actionCount } = await pageObjects.alertDetailsUI.getActionsLabels(); - expect(actionType).to.be(`Server log`); + expect(actionType).to.be(`Slack`); expect(actionCount).to.be(`+1`); }); @@ -168,7 +172,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const alert = await alerting.alerts.createAlertWithActions( testRunUuid, '.index-threshold', - params + params, + [ + { + group: 'threshold met', + id: 'my-server-log', + params: { level: 'info', message: ' {{context.message}}' }, + }, + ] ); // refresh to see alert await browser.refresh(); @@ -183,6 +194,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const editButton = await testSubjects.find('openEditAlertFlyoutButton'); await editButton.click(); + expect(await testSubjects.exists('hasActionsDisabled')).to.eql(false); const updatedAlertName = `Changed Alert Name ${uuid.v4()}`; await testSubjects.setValue('alertNameInput', updatedAlertName, { @@ -304,16 +316,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const actions = await Promise.all([ alerting.actions.createAction({ - name: `server-log-${testRunUuid}-${0}`, - actionTypeId: '.server-log', + name: `slack-${testRunUuid}-${0}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }), alerting.actions.createAction({ - name: `server-log-${testRunUuid}-${1}`, - actionTypeId: '.server-log', + name: `slack-${testRunUuid}-${1}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }), ]); @@ -516,16 +532,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const actions = await Promise.all([ alerting.actions.createAction({ - name: `server-log-${testRunUuid}-${0}`, - actionTypeId: '.server-log', + name: `slack-${testRunUuid}-${0}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }), alerting.actions.createAction({ - name: `server-log-${testRunUuid}-${1}`, - actionTypeId: '.server-log', + name: `slack-${testRunUuid}-${1}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }), ]); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index f049406b639c7..2edab1b164a1b 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -59,10 +59,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('navigates to an alert details page', async () => { const action = await alerting.actions.createAction({ - name: `server-log-${Date.now()}`, - actionTypeId: '.server-log', + name: `Slack-${Date.now()}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }); const alert = await alerting.alerts.createAlwaysFiringWithAction( diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 71b22a336f6b9..ef2270fb97745 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -10,6 +10,21 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; import { pageObjects } from './page_objects'; +// .server-log is specifically not enabled +const enabledActionTypes = [ + '.email', + '.index', + '.pagerduty', + '.servicenow', + '.slack', + '.webhook', + 'test.authorization', + 'test.failing', + 'test.index-record', + 'test.noop', + 'test.rate-limit', +]; + // eslint-disable-next-line import/no-default-export export default async function({ readConfigFile }: FtrConfigProviderContext) { const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); @@ -50,15 +65,21 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { `--elasticsearch.hosts=https://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, `--plugin-path=${join(__dirname, 'fixtures', 'plugins', 'alerts')}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, `--xpack.actions.preconfigured=${JSON.stringify([ { id: 'my-slack1', actionTypeId: '.slack', - name: 'Slack#xyz', + name: 'Slack#xyztest', config: { webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', }, }, + { + id: 'my-server-log', + actionTypeId: '.server-log', + name: 'Serverlog#xyz', + }, ])}`, ], }, From 728c34fc3c6a5d605b48b9c8ac56e18533c67c96 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 30 Apr 2020 23:58:46 -0700 Subject: [PATCH 063/122] [legacy/server/config] remove unnecessary deps for simple helper (#64954) * [legacy/server/config] remove unnecessary deps for simple helper * remove unnecessary change * expand test coverage a smidge * explode dot-separated keys * add a test for really deep keys Co-authored-by: spalger --- src/legacy/server/config/config.js | 2 +- src/legacy/server/config/explode_by.js | 37 ------ src/legacy/server/config/explode_by.test.js | 48 -------- src/legacy/server/config/override.js | 28 ----- src/legacy/server/config/override.test.js | 44 ------- src/legacy/server/config/override.test.ts | 130 ++++++++++++++++++++ src/legacy/server/config/override.ts | 52 ++++++++ 7 files changed, 183 insertions(+), 158 deletions(-) delete mode 100644 src/legacy/server/config/explode_by.js delete mode 100644 src/legacy/server/config/explode_by.test.js delete mode 100644 src/legacy/server/config/override.js delete mode 100644 src/legacy/server/config/override.test.js create mode 100644 src/legacy/server/config/override.test.ts create mode 100644 src/legacy/server/config/override.ts diff --git a/src/legacy/server/config/config.js b/src/legacy/server/config/config.js index c31ded608dd31..b186071edeaf7 100644 --- a/src/legacy/server/config/config.js +++ b/src/legacy/server/config/config.js @@ -19,7 +19,7 @@ import Joi from 'joi'; import _ from 'lodash'; -import override from './override'; +import { override } from './override'; import createDefaultSchema from './schema'; import { unset, deepCloneWithBuffers as clone, IS_KIBANA_DISTRIBUTABLE } from '../../utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/src/legacy/server/config/explode_by.js b/src/legacy/server/config/explode_by.js deleted file mode 100644 index 46347feca550d..0000000000000 --- a/src/legacy/server/config/explode_by.js +++ /dev/null @@ -1,37 +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'; - -export default function(dot, flatObject) { - const fullObject = {}; - _.each(flatObject, function(value, key) { - const keys = key.split(dot); - (function walk(memo, keys, value) { - const _key = keys.shift(); - if (keys.length === 0) { - memo[_key] = value; - } else { - if (!memo[_key]) memo[_key] = {}; - walk(memo[_key], keys, value); - } - })(fullObject, keys, value); - }); - return fullObject; -} diff --git a/src/legacy/server/config/explode_by.test.js b/src/legacy/server/config/explode_by.test.js deleted file mode 100644 index 741edba27d325..0000000000000 --- a/src/legacy/server/config/explode_by.test.js +++ /dev/null @@ -1,48 +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 explodeBy from './explode_by'; - -describe('explode_by(dot, flatObject)', function() { - it('should explode a flatten object with dots', function() { - const flatObject = { - 'test.enable': true, - 'test.hosts': ['host-01', 'host-02'], - }; - expect(explodeBy('.', flatObject)).toEqual({ - test: { - enable: true, - hosts: ['host-01', 'host-02'], - }, - }); - }); - - it('should explode a flatten object with slashes', function() { - const flatObject = { - 'test/enable': true, - 'test/hosts': ['host-01', 'host-02'], - }; - expect(explodeBy('/', flatObject)).toEqual({ - test: { - enable: true, - hosts: ['host-01', 'host-02'], - }, - }); - }); -}); diff --git a/src/legacy/server/config/override.js b/src/legacy/server/config/override.js deleted file mode 100644 index bab9387ac006f..0000000000000 --- a/src/legacy/server/config/override.js +++ /dev/null @@ -1,28 +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 explodeBy from './explode_by'; -import { getFlattenedObject } from '../../../core/utils'; - -export default function(target, source) { - const _target = getFlattenedObject(target); - const _source = getFlattenedObject(source); - return explodeBy('.', _.defaults(_source, _target)); -} diff --git a/src/legacy/server/config/override.test.js b/src/legacy/server/config/override.test.js deleted file mode 100644 index 331c586e28a87..0000000000000 --- a/src/legacy/server/config/override.test.js +++ /dev/null @@ -1,44 +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 override from './override'; - -describe('override(target, source)', function() { - it('should override the values form source to target', function() { - const target = { - test: { - enable: true, - host: ['host-01', 'host-02'], - client: { - type: 'sql', - }, - }, - }; - const source = { test: { client: { type: 'nosql' } } }; - expect(override(target, source)).toEqual({ - test: { - enable: true, - host: ['host-01', 'host-02'], - client: { - type: 'nosql', - }, - }, - }); - }); -}); diff --git a/src/legacy/server/config/override.test.ts b/src/legacy/server/config/override.test.ts new file mode 100644 index 0000000000000..4e21a88e79e61 --- /dev/null +++ b/src/legacy/server/config/override.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { override } from './override'; + +describe('override(target, source)', function() { + it('should override the values form source to target', function() { + const target = { + test: { + enable: true, + host: ['something else'], + client: { + type: 'sql', + }, + }, + }; + + const source = { + test: { + host: ['host-01', 'host-02'], + client: { + type: 'nosql', + }, + foo: { + bar: { + baz: 1, + }, + }, + }, + }; + + expect(override(target, source)).toMatchInlineSnapshot(` + Object { + "test": Object { + "client": Object { + "type": "nosql", + }, + "enable": true, + "foo": Object { + "bar": Object { + "baz": 1, + }, + }, + "host": Array [ + "host-01", + "host-02", + ], + }, + } + `); + }); + + it('does not mutate arguments', () => { + const target = { + foo: { + bar: 1, + baz: 1, + }, + }; + + const source = { + foo: { + bar: 2, + }, + box: 2, + }; + + expect(override(target, source)).toMatchInlineSnapshot(` + Object { + "box": 2, + "foo": Object { + "bar": 2, + "baz": 1, + }, + } + `); + expect(target).not.toHaveProperty('box'); + expect(source.foo).not.toHaveProperty('baz'); + }); + + it('explodes keys with dots in them', () => { + const target = { + foo: { + bar: 1, + }, + 'baz.box.boot.bar.bar': 20, + }; + + const source = { + 'foo.bar': 2, + 'baz.box.boot': { + 'bar.foo': 10, + }, + }; + + expect(override(target, source)).toMatchInlineSnapshot(` + Object { + "baz": Object { + "box": Object { + "boot": Object { + "bar": Object { + "bar": 20, + "foo": 10, + }, + }, + }, + }, + "foo": Object { + "bar": 2, + }, + } + `); + }); +}); diff --git a/src/legacy/server/config/override.ts b/src/legacy/server/config/override.ts new file mode 100644 index 0000000000000..3dd7d62016004 --- /dev/null +++ b/src/legacy/server/config/override.ts @@ -0,0 +1,52 @@ +/* + * 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. + */ + +const isObject = (v: any): v is Record => + typeof v === 'object' && v !== null && !Array.isArray(v); + +const assignDeep = (target: Record, source: Record) => { + for (let [key, value] of Object.entries(source)) { + // unwrap dot-separated keys + if (key.includes('.')) { + const [first, ...others] = key.split('.'); + key = first; + value = { [others.join('.')]: value }; + } + + if (isObject(value)) { + if (!target.hasOwnProperty(key)) { + target[key] = {}; + } + + assignDeep(target[key], value); + } else { + target[key] = value; + } + } +}; + +export const override = (...sources: Array>): Record => { + const result = {}; + + for (const object of sources) { + assignDeep(result, object); + } + + return result; +}; From c8ddb6b8edefaaf88f028f7ab46d3ace16218122 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 1 May 2020 12:57:29 +0200 Subject: [PATCH 064/122] load lens app lazily (#64769) --- .../lens/public/app_plugin/mounter.tsx | 118 ++++++++++ x-pack/plugins/lens/public/helpers/index.ts | 1 + .../lens/public/helpers/is_rison_object.ts | 12 + x-pack/plugins/lens/public/plugin.ts | 103 +++++++++ x-pack/plugins/lens/public/plugin.tsx | 209 ------------------ 5 files changed, 234 insertions(+), 209 deletions(-) create mode 100644 x-pack/plugins/lens/public/app_plugin/mounter.tsx create mode 100644 x-pack/plugins/lens/public/helpers/is_rison_object.ts create mode 100644 x-pack/plugins/lens/public/plugin.ts delete mode 100644 x-pack/plugins/lens/public/plugin.tsx diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx new file mode 100644 index 0000000000000..f295f88a58e5f --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -0,0 +1,118 @@ +/* + * 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 { AppMountParameters, CoreSetup } from 'kibana/public'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import rison from 'rison-node'; +import { DashboardConstants } from '../../../../../src/plugins/dashboard/public'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; + +import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry'; + +import { App } from './app'; +import { EditorFrameStart } from '../types'; +import { addEmbeddableToDashboardUrl, getUrlVars, isRisonObject } from '../helpers'; +import { addHelpMenuToAppChrome } from '../help_menu_util'; +import { SavedObjectIndexStore } from '../persistence'; +import { LensPluginStartDependencies } from '../plugin'; + +export async function mountApp( + core: CoreSetup, + params: AppMountParameters, + createEditorFrame: EditorFrameStart['createInstance'] +) { + const [coreStart, startDependencies] = await core.getStartServices(); + const { data: dataStart, navigation } = startDependencies; + const savedObjectsClient = coreStart.savedObjects.client; + addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks); + + const instance = await createEditorFrame(); + + setReportManager( + new LensReportManager({ + storage: new Storage(localStorage), + http: core.http, + }) + ); + const updateUrlTime = (urlVars: Record): void => { + const decoded = rison.decode(urlVars._g); + if (!isRisonObject(decoded)) { + return; + } + // @ts-ignore + decoded.time = dataStart.query.timefilter.timefilter.getTime(); + urlVars._g = rison.encode(decoded); + }; + const redirectTo = ( + routeProps: RouteComponentProps<{ id?: string }>, + addToDashboardMode: boolean, + id?: string + ) => { + if (!id) { + routeProps.history.push('/lens'); + } else if (!addToDashboardMode) { + routeProps.history.push(`/lens/edit/${id}`); + } else if (addToDashboardMode && id) { + routeProps.history.push(`/lens/edit/${id}`); + const lastDashboardLink = coreStart.chrome.navLinks.get('kibana:dashboard'); + if (!lastDashboardLink || !lastDashboardLink.url) { + throw new Error('Cannot get last dashboard url'); + } + const urlVars = getUrlVars(lastDashboardLink.url); + updateUrlTime(urlVars); // we need to pass in timerange in query params directly + const dashboardUrl = addEmbeddableToDashboardUrl(lastDashboardLink.url, id, urlVars); + window.history.pushState({}, '', dashboardUrl); + } + }; + + const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { + trackUiEvent('loaded'); + const addToDashboardMode = + !!routeProps.location.search && + routeProps.location.search.includes( + DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM + ); + return ( + redirectTo(routeProps, addToDashboardMode, id)} + addToDashboardMode={addToDashboardMode} + /> + ); + }; + + function NotFound() { + trackUiEvent('loaded_404'); + return ; + } + + render( + + + + + + + + + , + params.element + ); + return () => { + instance.unmount(); + unmountComponentAtNode(params.element); + }; +} diff --git a/x-pack/plugins/lens/public/helpers/index.ts b/x-pack/plugins/lens/public/helpers/index.ts index f464b5dcc97a3..69a22d19ffbef 100644 --- a/x-pack/plugins/lens/public/helpers/index.ts +++ b/x-pack/plugins/lens/public/helpers/index.ts @@ -5,3 +5,4 @@ */ export { addEmbeddableToDashboardUrl, getUrlVars } from './url_helper'; +export { isRisonObject } from './is_rison_object'; diff --git a/x-pack/plugins/lens/public/helpers/is_rison_object.ts b/x-pack/plugins/lens/public/helpers/is_rison_object.ts new file mode 100644 index 0000000000000..81976c9a83320 --- /dev/null +++ b/x-pack/plugins/lens/public/helpers/is_rison_object.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 { RisonObject, RisonValue } from 'rison-node'; +import { isObject } from 'lodash'; + +export const isRisonObject = (value: RisonValue): value is RisonObject => { + return isObject(value); +}; diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts new file mode 100644 index 0000000000000..a6acc61922177 --- /dev/null +++ b/x-pack/plugins/lens/public/plugin.ts @@ -0,0 +1,103 @@ +/* + * 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 { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; +import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; +import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; +import { VisualizationsSetup } from 'src/plugins/visualizations/public'; +import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import { KibanaLegacySetup } from 'src/plugins/kibana_legacy/public'; +import { EditorFrameService } from './editor_frame_service'; +import { IndexPatternDatasource } from './indexpattern_datasource'; +import { XyVisualization } from './xy_visualization'; +import { MetricVisualization } from './metric_visualization'; +import { DatatableVisualization } from './datatable_visualization'; +import { stopReportManager } from './lens_ui_telemetry'; + +import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; +import { EditorFrameStart } from './types'; +import { getLensAliasConfig } from './vis_type_alias'; + +import './index.scss'; + +export interface LensPluginSetupDependencies { + kibanaLegacy: KibanaLegacySetup; + expressions: ExpressionsSetup; + data: DataPublicPluginSetup; + embeddable?: EmbeddableSetup; + visualizations: VisualizationsSetup; +} + +export interface LensPluginStartDependencies { + data: DataPublicPluginStart; + embeddable: EmbeddableStart; + expressions: ExpressionsStart; + navigation: NavigationPublicPluginStart; + uiActions: UiActionsStart; +} + +export class LensPlugin { + private datatableVisualization: DatatableVisualization; + private editorFrameService: EditorFrameService; + private createEditorFrame: EditorFrameStart['createInstance'] | null = null; + private indexpatternDatasource: IndexPatternDatasource; + private xyVisualization: XyVisualization; + private metricVisualization: MetricVisualization; + + constructor() { + this.datatableVisualization = new DatatableVisualization(); + this.editorFrameService = new EditorFrameService(); + this.indexpatternDatasource = new IndexPatternDatasource(); + this.xyVisualization = new XyVisualization(); + this.metricVisualization = new MetricVisualization(); + } + + setup( + core: CoreSetup, + { kibanaLegacy, expressions, data, embeddable, visualizations }: LensPluginSetupDependencies + ) { + const editorFrameSetupInterface = this.editorFrameService.setup(core, { + data, + embeddable, + expressions, + }); + const dependencies = { + expressions, + data, + editorFrame: editorFrameSetupInterface, + formatFactory: core + .getStartServices() + .then(([_, { data: dataStart }]) => dataStart.fieldFormats.deserialize), + }; + this.indexpatternDatasource.setup(core, dependencies); + this.xyVisualization.setup(core, dependencies); + this.datatableVisualization.setup(core, dependencies); + this.metricVisualization.setup(core, dependencies); + + visualizations.registerAlias(getLensAliasConfig()); + + kibanaLegacy.registerLegacyApp({ + id: 'lens', + title: NOT_INTERNATIONALIZED_PRODUCT_NAME, + mount: async (params: AppMountParameters) => { + const { mountApp } = await import('./app_plugin/mounter'); + return mountApp(core, params, this.createEditorFrame!); + }, + }); + } + + start(core: CoreStart, startDependencies: LensPluginStartDependencies) { + this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance; + this.xyVisualization.start(core, startDependencies); + this.datatableVisualization.start(core, startDependencies); + } + + stop() { + stopReportManager(); + } +} diff --git a/x-pack/plugins/lens/public/plugin.tsx b/x-pack/plugins/lens/public/plugin.tsx deleted file mode 100644 index fe0e81177e259..0000000000000 --- a/x-pack/plugins/lens/public/plugin.tsx +++ /dev/null @@ -1,209 +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 { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { render, unmountComponentAtNode } from 'react-dom'; -import rison, { RisonObject, RisonValue } from 'rison-node'; -import { isObject } from 'lodash'; - -import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; -import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; -import { VisualizationsSetup } from 'src/plugins/visualizations/public'; -import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; -import { KibanaLegacySetup } from 'src/plugins/kibana_legacy/public'; -import { DashboardConstants } from '../../../../src/plugins/dashboard/public'; -import { Storage } from '../../../../src/plugins/kibana_utils/public'; -import { EditorFrameService } from './editor_frame_service'; -import { IndexPatternDatasource } from './indexpattern_datasource'; -import { addHelpMenuToAppChrome } from './help_menu_util'; -import { SavedObjectIndexStore } from './persistence'; -import { XyVisualization } from './xy_visualization'; -import { MetricVisualization } from './metric_visualization'; -import { DatatableVisualization } from './datatable_visualization'; -import { App } from './app_plugin'; -import { - LensReportManager, - setReportManager, - stopReportManager, - trackUiEvent, -} from './lens_ui_telemetry'; - -import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; -import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; -import { addEmbeddableToDashboardUrl, getUrlVars } from './helpers'; -import { EditorFrameStart } from './types'; -import { getLensAliasConfig } from './vis_type_alias'; - -import './index.scss'; - -export interface LensPluginSetupDependencies { - kibanaLegacy: KibanaLegacySetup; - expressions: ExpressionsSetup; - data: DataPublicPluginSetup; - embeddable?: EmbeddableSetup; - visualizations: VisualizationsSetup; -} - -export interface LensPluginStartDependencies { - data: DataPublicPluginStart; - embeddable: EmbeddableStart; - expressions: ExpressionsStart; - navigation: NavigationPublicPluginStart; - uiActions: UiActionsStart; -} - -export const isRisonObject = (value: RisonValue): value is RisonObject => { - return isObject(value); -}; -export class LensPlugin { - private datatableVisualization: DatatableVisualization; - private editorFrameService: EditorFrameService; - private createEditorFrame: EditorFrameStart['createInstance'] | null = null; - private indexpatternDatasource: IndexPatternDatasource; - private xyVisualization: XyVisualization; - private metricVisualization: MetricVisualization; - - constructor() { - this.datatableVisualization = new DatatableVisualization(); - this.editorFrameService = new EditorFrameService(); - this.indexpatternDatasource = new IndexPatternDatasource(); - this.xyVisualization = new XyVisualization(); - this.metricVisualization = new MetricVisualization(); - } - - setup( - core: CoreSetup, - { kibanaLegacy, expressions, data, embeddable, visualizations }: LensPluginSetupDependencies - ) { - const editorFrameSetupInterface = this.editorFrameService.setup(core, { - data, - embeddable, - expressions, - }); - const dependencies = { - expressions, - data, - editorFrame: editorFrameSetupInterface, - formatFactory: core - .getStartServices() - .then(([_, { data: dataStart }]) => dataStart.fieldFormats.deserialize), - }; - this.indexpatternDatasource.setup(core, dependencies); - this.xyVisualization.setup(core, dependencies); - this.datatableVisualization.setup(core, dependencies); - this.metricVisualization.setup(core, dependencies); - - visualizations.registerAlias(getLensAliasConfig()); - - kibanaLegacy.registerLegacyApp({ - id: 'lens', - title: NOT_INTERNATIONALIZED_PRODUCT_NAME, - mount: async (params: AppMountParameters) => { - const [coreStart, startDependencies] = await core.getStartServices(); - const { data: dataStart, navigation } = startDependencies; - const savedObjectsClient = coreStart.savedObjects.client; - addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks); - - const instance = await this.createEditorFrame!(); - - setReportManager( - new LensReportManager({ - storage: new Storage(localStorage), - http: core.http, - }) - ); - const updateUrlTime = (urlVars: Record): void => { - const decoded = rison.decode(urlVars._g); - if (!isRisonObject(decoded)) { - return; - } - // @ts-ignore - decoded.time = dataStart.query.timefilter.timefilter.getTime(); - urlVars._g = rison.encode(decoded); - }; - const redirectTo = ( - routeProps: RouteComponentProps<{ id?: string }>, - addToDashboardMode: boolean, - id?: string - ) => { - if (!id) { - routeProps.history.push('/lens'); - } else if (!addToDashboardMode) { - routeProps.history.push(`/lens/edit/${id}`); - } else if (addToDashboardMode && id) { - routeProps.history.push(`/lens/edit/${id}`); - const lastDashboardLink = coreStart.chrome.navLinks.get('kibana:dashboard'); - if (!lastDashboardLink || !lastDashboardLink.url) { - throw new Error('Cannot get last dashboard url'); - } - const urlVars = getUrlVars(lastDashboardLink.url); - updateUrlTime(urlVars); // we need to pass in timerange in query params directly - const dashboardUrl = addEmbeddableToDashboardUrl(lastDashboardLink.url, id, urlVars); - window.history.pushState({}, '', dashboardUrl); - } - }; - - const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { - trackUiEvent('loaded'); - const addToDashboardMode = - !!routeProps.location.search && - routeProps.location.search.includes( - DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM - ); - return ( - redirectTo(routeProps, addToDashboardMode, id)} - addToDashboardMode={addToDashboardMode} - /> - ); - }; - - function NotFound() { - trackUiEvent('loaded_404'); - return ; - } - - render( - - - - - - - - - , - params.element - ); - return () => { - instance.unmount(); - unmountComponentAtNode(params.element); - }; - }, - }); - } - - start(core: CoreStart, startDependencies: LensPluginStartDependencies) { - this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance; - this.xyVisualization.start(core, startDependencies); - this.datatableVisualization.start(core, startDependencies); - } - - stop() { - stopReportManager(); - } -} From 523926f0a49a5bd951ed0e056610b049482adbca Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 1 May 2020 16:04:19 +0200 Subject: [PATCH 065/122] [Uptime] Certificates page (#64059) --- x-pack/plugins/uptime/common/constants/ui.ts | 9 + .../uptime/common/runtime_types/certs.ts | 30 +- .../uptime/common/runtime_types/ping/ping.ts | 4 +- .../__snapshots__/cert_monitors.test.tsx.snap | 134 + .../__snapshots__/cert_search.test.tsx.snap | 93 + .../__snapshots__/cert_status.test.tsx.snap | 100 + .../certificates_list.test.tsx.snap | 70 + .../fingerprint_col.test.tsx.snap | 171 ++ .../__tests__/cert_monitors.test.tsx | 24 + .../__tests__/cert_search.test.tsx | 18 + .../__tests__/cert_status.test.tsx | 42 + .../__tests__/certificates_list.test.tsx | 26 + .../__tests__/fingerprint_col.test.tsx | 34 + .../components/certificates/cert_monitors.tsx | 31 + .../components/certificates/cert_search.tsx | 34 + .../components/certificates/cert_status.tsx | 49 + .../certificates/certificates_list.tsx | 113 + .../certificates/fingerprint_col.tsx | 46 + .../public/components/certificates/index.ts | 11 + .../components/certificates/translations.ts | 63 + .../monitor_page_link.test.tsx.snap | 0 .../__tests__/monitor_page_link.test.tsx | 2 +- .../monitor_page_link.tsx | 4 +- .../monitor_ssl_certificate.test.tsx.snap | 31 - .../ssl_certificate.test.tsx.snap | 119 + .../__test__/monitor_status.bar.test.tsx | 7 + ...cate.test.tsx => ssl_certificate.test.tsx} | 46 +- .../monitor_status_bar/ssl_certificate.tsx | 102 +- .../__snapshots__/monitor_list.test.tsx.snap | 84 +- .../__tests__/monitor_list.test.tsx | 7 + .../monitor_list/cert_status_column.tsx | 51 + .../overview/monitor_list/monitor_list.tsx | 46 +- .../monitor_list_drawer/most_recent_error.tsx | 2 +- .../overview/monitor_list/translations.ts | 4 + x-pack/plugins/uptime/public/hooks/index.ts | 1 + .../uptime/public/hooks/use_cert_status.ts | 42 + .../uptime/public/hooks/use_telemetry.ts | 1 + .../__snapshots__/certificates.test.tsx.snap | 56 + .../pages/__tests__/certificates.test.tsx | 15 + .../uptime/public/pages/certificates.tsx | 136 + .../plugins/uptime/public/pages/monitor.tsx | 11 +- .../uptime/public/pages/page_header.tsx | 2 +- .../uptime/public/pages/translations.ts | 15 + x-pack/plugins/uptime/public/routes.tsx | 13 +- .../uptime/public/state/api/certificates.ts | 13 + .../public/state/certificates/certificates.ts | 43 + .../uptime/public/state/effects/index.ts | 2 + .../uptime/public/state/reducers/index.ts | 2 + .../public/state/reducers/ml_anomaly.ts | 13 +- .../public/state/reducers/monitor_list.ts | 2 +- .../uptime/public/state/reducers/utils.ts | 4 +- .../state/selectors/__tests__/index.test.ts | 6 + .../lib/requests/__tests__/get_certs.test.ts | 104 +- .../__tests__/get_latest_monitor.test.ts | 13 +- .../uptime/server/lib/requests/get_certs.ts | 110 +- .../server/lib/requests/get_latest_monitor.ts | 18 +- .../requests/search/enrich_monitor_groups.ts | 8 +- .../server/lib/requests/uptime_requests.ts | 15 +- .../server/rest_api/{ => certs}/certs.ts | 39 +- .../plugins/uptime/server/rest_api/index.ts | 3 +- .../api_integration/apis/uptime/rest/certs.ts | 15 +- .../rest/fixtures/monitor_latest_status.json | 5 +- .../apis/uptime/rest/helper/make_checks.ts | 154 +- .../apis/uptime/rest/helper/make_ping.ts | 118 + .../apis/uptime/rest/helper/make_tls.ts | 70 + .../functional/apps/uptime/certificates.ts | 62 + x-pack/test/functional/apps/uptime/index.ts | 1 + .../es_archives/uptime/blank/mappings.json | 44 +- .../uptime/full_heartbeat/mappings.json | 2664 +++++++++++++---- .../functional/page_objects/uptime_page.ts | 5 +- .../services/uptime/certificates.ts | 50 + .../functional/services/uptime/navigation.ts | 13 +- .../test/functional/services/uptime/uptime.ts | 3 + 73 files changed, 4489 insertions(+), 1009 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_monitors.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_search.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_status.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/certificates_list.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/certificates/__tests__/cert_monitors.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/certificates/__tests__/cert_search.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/certificates/__tests__/cert_status.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/certificates/__tests__/certificates_list.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/certificates/__tests__/fingerprint_col.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/certificates/cert_monitors.tsx create mode 100644 x-pack/plugins/uptime/public/components/certificates/cert_search.tsx create mode 100644 x-pack/plugins/uptime/public/components/certificates/cert_status.tsx create mode 100644 x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx create mode 100644 x-pack/plugins/uptime/public/components/certificates/fingerprint_col.tsx create mode 100644 x-pack/plugins/uptime/public/components/certificates/index.ts create mode 100644 x-pack/plugins/uptime/public/components/certificates/translations.ts rename x-pack/plugins/uptime/public/components/{overview/monitor_list => common}/__tests__/__snapshots__/monitor_page_link.test.tsx.snap (100%) rename x-pack/plugins/uptime/public/components/{overview/monitor_list => common}/__tests__/monitor_page_link.test.tsx (100%) rename x-pack/plugins/uptime/public/components/{overview/monitor_list => common}/monitor_page_link.tsx (89%) delete mode 100644 x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap rename x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/{monitor_ssl_certificate.test.tsx => ssl_certificate.test.tsx} (55%) create mode 100644 x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx create mode 100644 x-pack/plugins/uptime/public/hooks/use_cert_status.ts create mode 100644 x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/certificates.test.tsx.snap create mode 100644 x-pack/plugins/uptime/public/pages/__tests__/certificates.test.tsx create mode 100644 x-pack/plugins/uptime/public/pages/certificates.tsx create mode 100644 x-pack/plugins/uptime/public/state/api/certificates.ts create mode 100644 x-pack/plugins/uptime/public/state/certificates/certificates.ts rename x-pack/plugins/uptime/server/rest_api/{ => certs}/certs.ts (63%) create mode 100644 x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts create mode 100644 x-pack/test/api_integration/apis/uptime/rest/helper/make_tls.ts create mode 100644 x-pack/test/functional/apps/uptime/certificates.ts create mode 100644 x-pack/test/functional/services/uptime/certificates.ts diff --git a/x-pack/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts index 29e8dabf53f92..3bf3e3cc0a2cc 100644 --- a/x-pack/plugins/uptime/common/constants/ui.ts +++ b/x-pack/plugins/uptime/common/constants/ui.ts @@ -10,6 +10,8 @@ export const OVERVIEW_ROUTE = '/'; export const SETTINGS_ROUTE = '/settings'; +export const CERTIFICATES_ROUTE = '/certificates'; + export enum STATUS { UP = 'up', DOWN = 'down', @@ -41,3 +43,10 @@ export const SHORT_TIMESPAN_LOCALE = { yy: '%d Yr', }, }; + +export enum CERT_STATUS { + OK = 'OK', + EXPIRING_SOON = 'EXPIRING_SOON', + EXPIRED = 'EXPIRED', + TOO_OLD = 'TOO_OLD', +} diff --git a/x-pack/plugins/uptime/common/runtime_types/certs.ts b/x-pack/plugins/uptime/common/runtime_types/certs.ts index e8be67abf3a44..e9071e76b6d75 100644 --- a/x-pack/plugins/uptime/common/runtime_types/certs.ts +++ b/x-pack/plugins/uptime/common/runtime_types/certs.ts @@ -8,35 +8,45 @@ import * as t from 'io-ts'; export const GetCertsParamsType = t.intersection([ t.type({ - from: t.string, - to: t.string, index: t.number, size: t.number, + sortBy: t.string, + direction: t.string, }), t.partial({ search: t.string, + from: t.string, + to: t.string, }), ]); export type GetCertsParams = t.TypeOf; +export const CertMonitorType = t.partial({ + name: t.string, + id: t.string, + url: t.string, +}); + export const CertType = t.intersection([ t.type({ - monitors: t.array( - t.partial({ - name: t.string, - id: t.string, - }) - ), + monitors: t.array(CertMonitorType), sha256: t.string, }), t.partial({ - certificate_not_valid_after: t.string, - certificate_not_valid_before: t.string, + not_after: t.string, + not_before: t.string, common_name: t.string, issuer: t.string, sha1: t.string, }), ]); +export const CertResultType = t.type({ + certs: t.array(CertType), + total: t.number, +}); + export type Cert = t.TypeOf; +export type CertMonitor = t.TypeOf; +export type CertResult = t.TypeOf; diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index ee14b298f3810..d8dc7fc89d94b 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -17,8 +17,8 @@ export const HttpResponseBodyType = t.partial({ export type HttpResponseBody = t.TypeOf; export const TlsType = t.partial({ - certificate_not_valid_after: t.string, - certificate_not_valid_before: t.string, + not_after: t.string, + not_before: t.string, }); export type Tls = t.TypeOf; diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_monitors.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_monitors.test.tsx.snap new file mode 100644 index 0000000000000..a79fb0f0d3deb --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_monitors.test.tsx.snap @@ -0,0 +1,134 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertMonitors renders expected elements for valid props 1`] = ` + + + + + + + + , + + + + + + , + + + + + +`; + +exports[`CertMonitors shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_search.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_search.test.tsx.snap new file mode 100644 index 0000000000000..0706198a099a5 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_search.test.tsx.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertificatesSearch renders expected elements for valid props 1`] = ` +.c0 { + min-width: 700px; +} + +

    +
    + +
    + + +
    +
    +`; + +exports[`CertificatesSearch shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_status.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_status.test.tsx.snap new file mode 100644 index 0000000000000..089d272a075c6 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_status.test.tsx.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertStatus renders expected elements for valid props 1`] = ` +
    +
    +
    +
    +
    +
    + + OK + +
    +
    +
    +`; + +exports[`CertStatus shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/certificates_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/certificates_list.test.tsx.snap new file mode 100644 index 0000000000000..fd90db793b26e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/certificates_list.test.tsx.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertificateList shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap new file mode 100644 index 0000000000000..c9b17db5532f4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FingerprintCol renders expected elements for valid props 1`] = ` +Array [ + .c1 .euiButtonEmpty__content { + padding-right: 0px; +} + +.c0 { + margin-right: 8px; +} + + + + + + + + + , + .c1 .euiButtonEmpty__content { + padding-right: 0px; +} + +.c0 { + margin-right: 8px; +} + + + + + + + + + , +] +`; + +exports[`FingerprintCol shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_monitors.test.tsx b/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_monitors.test.tsx new file mode 100644 index 0000000000000..bc4c770d5cd24 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_monitors.test.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. + */ + +import React from 'react'; +import { CertMonitors } from '../cert_monitors'; +import { renderWithRouter, shallowWithRouter } from '../../../lib'; + +describe('CertMonitors', () => { + const certMons = [ + { name: '', id: 'bad-ssl-dashboard', url: 'https://badssl.com/dashboard/' }, + { name: 'elastic', id: 'elastic-co', url: 'https://www.elastic.co/' }, + { name: '', id: 'extended-validation', url: 'https://extended-validation.badssl.com/' }, + ]; + it('shallow renders expected elements for valid props', () => { + expect(shallowWithRouter()).toMatchSnapshot(); + }); + + it('renders expected elements for valid props', () => { + expect(renderWithRouter()).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_search.test.tsx b/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_search.test.tsx new file mode 100644 index 0000000000000..27d3bb18f17c2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_search.test.tsx @@ -0,0 +1,18 @@ +/* + * 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 { renderWithRouter, shallowWithRouter } from '../../../lib'; +import { CertificateSearch } from '../cert_search'; + +describe('CertificatesSearch', () => { + it('shallow renders expected elements for valid props', () => { + expect(shallowWithRouter()).toMatchSnapshot(); + }); + it('renders expected elements for valid props', () => { + expect(renderWithRouter()).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_status.test.tsx b/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_status.test.tsx new file mode 100644 index 0000000000000..6f91994fb89c4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_status.test.tsx @@ -0,0 +1,42 @@ +/* + * 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 { renderWithRouter, shallowWithRouter } from '../../../lib'; +import { CertStatus } from '../cert_status'; +import * as redux from 'react-redux'; +import moment from 'moment'; + +describe('CertStatus', () => { + beforeEach(() => { + const spy = jest.spyOn(redux, 'useDispatch'); + spy.mockReturnValue(jest.fn()); + + const spy1 = jest.spyOn(redux, 'useSelector'); + spy1.mockReturnValue(true); + }); + + const cert = { + monitors: [{ name: '', id: 'github', url: 'https://github.com/' }], + not_after: '2020-05-08T00:00:00.000Z', + not_before: '2018-05-08T00:00:00.000Z', + issuer: 'DigiCert SHA2 Extended Validation Server CA', + sha1: 'ca06f56b258b7a0d4f2b05470939478651151984', + sha256: '3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074', + common_name: 'github.com', + }; + + it('shallow renders expected elements for valid props', () => { + expect(shallowWithRouter()).toMatchSnapshot(); + }); + + it('renders expected elements for valid props', () => { + cert.not_after = moment() + .add('4', 'months') + .toISOString(); + expect(renderWithRouter()).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/certificates_list.test.tsx b/x-pack/plugins/uptime/public/components/certificates/__tests__/certificates_list.test.tsx new file mode 100644 index 0000000000000..a8b60900ec65c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/certificates_list.test.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'; +import { shallowWithRouter } from '../../../lib'; +import { CertificateList, CertSort } from '../certificates_list'; + +describe('CertificateList', () => { + it('shallow renders expected elements for valid props', () => { + const page = { + index: 0, + size: 10, + }; + const sort: CertSort = { + field: 'not_after', + direction: 'asc', + }; + + expect( + shallowWithRouter() + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/fingerprint_col.test.tsx b/x-pack/plugins/uptime/public/components/certificates/__tests__/fingerprint_col.test.tsx new file mode 100644 index 0000000000000..609b876e24849 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/fingerprint_col.test.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 { renderWithRouter, shallowWithRouter } from '../../../lib'; +import { FingerprintCol } from '../fingerprint_col'; +import moment from 'moment'; + +describe('FingerprintCol', () => { + const cert = { + monitors: [{ name: '', id: 'github', url: 'https://github.com/' }], + not_after: '2020-05-08T00:00:00.000Z', + not_before: '2018-05-08T00:00:00.000Z', + issuer: 'DigiCert SHA2 Extended Validation Server CA', + sha1: 'ca06f56b258b7a0d4f2b05470939478651151984', + sha256: '3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074', + common_name: 'github.com', + }; + + it('shallow renders expected elements for valid props', () => { + expect(shallowWithRouter()).toMatchSnapshot(); + }); + + it('renders expected elements for valid props', () => { + cert.not_after = moment() + .add('4', 'months') + .toISOString(); + + expect(renderWithRouter()).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_monitors.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_monitors.tsx new file mode 100644 index 0000000000000..bfd309e59d013 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/cert_monitors.tsx @@ -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. + */ + +import React from 'react'; +import { EuiToolTip } from '@elastic/eui'; +import { CertMonitor } from '../../../common/runtime_types'; +import { MonitorPageLink } from '../common/monitor_page_link'; + +interface Props { + monitors: CertMonitor[]; +} + +export const CertMonitors: React.FC = ({ monitors }) => { + return ( + + {monitors.map((mon: CertMonitor, ind: number) => ( + + {ind > 0 && ', '} + + + {mon.name || mon.id} + + + + ))} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_search.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_search.tsx new file mode 100644 index 0000000000000..282b623f0f662 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/cert_search.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, { ChangeEvent } from 'react'; +import { EuiFieldSearch } from '@elastic/eui'; +import styled from 'styled-components'; +import * as labels from './translations'; + +const WrapFieldSearch = styled(EuiFieldSearch)` + min-width: 700px; +`; + +interface Props { + setSearch: (val: string) => void; +} + +export const CertificateSearch: React.FC = ({ setSearch }) => { + const onChange = (e: ChangeEvent) => { + setSearch(e.target.value); + }; + + return ( + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_status.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_status.tsx new file mode 100644 index 0000000000000..e7a86ce98fa3c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/cert_status.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EuiHealth } from '@elastic/eui'; +import { Cert } from '../../../common/runtime_types'; +import { useCertStatus } from '../../hooks'; +import * as labels from './translations'; +import { CERT_STATUS } from '../../../common/constants'; + +interface Props { + cert: Cert; +} + +export const CertStatus: React.FC = ({ cert }) => { + const certStatus = useCertStatus(cert?.not_after, cert?.not_before); + + if (certStatus === CERT_STATUS.EXPIRING_SOON) { + return ( + + {labels.EXPIRES_SOON} + + ); + } + if (certStatus === CERT_STATUS.EXPIRED) { + return ( + + {labels.EXPIRED} + + ); + } + + if (certStatus === CERT_STATUS.TOO_OLD) { + return ( + + {labels.TOO_OLD} + + ); + } + + return ( + + {labels.OK} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx b/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx new file mode 100644 index 0000000000000..595aa03c99c73 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx @@ -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 React from 'react'; +import moment from 'moment'; +import { useSelector } from 'react-redux'; +import { Direction, EuiBasicTable } from '@elastic/eui'; +import { certificatesSelector } from '../../state/certificates/certificates'; +import { CertStatus } from './cert_status'; +import { CertMonitors } from './cert_monitors'; +import * as labels from './translations'; +import { Cert, CertMonitor } from '../../../common/runtime_types'; +import { FingerprintCol } from './fingerprint_col'; + +interface Page { + index: number; + size: number; +} + +export type CertFields = + | 'sha256' + | 'sha1' + | 'issuer' + | 'common_name' + | 'monitors' + | 'not_after' + | 'not_before'; + +export interface CertSort { + field: CertFields; + direction: Direction; +} + +interface Props { + page: Page; + sort: CertSort; + onChange: (page: Page, sort: CertSort) => void; +} + +export const CertificateList: React.FC = ({ page, sort, onChange }) => { + const certificates = useSelector(certificatesSelector); + + const onTableChange = (newVal: Partial) => { + onChange(newVal.page as Page, newVal.sort as CertSort); + }; + + const pagination = { + pageIndex: page.index, + pageSize: page.size, + totalItemCount: certificates?.total ?? 0, + pageSizeOptions: [10, 25, 50, 100], + hidePerPageOptions: false, + }; + + const columns = [ + { + field: 'not_after', + name: labels.STATUS_COL, + sortable: true, + render: (val: string, item: Cert) => , + }, + { + name: labels.COMMON_NAME_COL, + field: 'common_name', + sortable: true, + }, + { + name: labels.MONITORS_COL, + field: 'monitors', + render: (monitors: CertMonitor[]) => , + }, + { + name: labels.ISSUED_BY_COL, + field: 'issuer', + sortable: true, + }, + { + name: labels.VALID_UNTIL_COL, + field: 'not_after', + sortable: true, + render: (value: string) => moment(value).format('L LT'), + }, + { + name: labels.AGE_COL, + field: 'not_before', + sortable: true, + render: (value: string) => moment().diff(moment(value), 'days') + ' ' + labels.DAYS, + }, + { + name: labels.FINGERPRINTS_COL, + field: 'sha256', + render: (val: string, item: Cert) => , + }, + ]; + + return ( + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.tsx b/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.tsx new file mode 100644 index 0000000000000..4101573907924 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiButtonIcon, EuiCopy, EuiToolTip } from '@elastic/eui'; +import styled from 'styled-components'; +import { Cert } from '../../../common/runtime_types'; +import { COPY_FINGERPRINT } from './translations'; + +const EmptyButton = styled(EuiButtonEmpty)` + .euiButtonEmpty__content { + padding-right: 0px; + } +`; + +const Span = styled.span` + margin-right: 8px; +`; + +interface Props { + cert: Cert; +} + +export const FingerprintCol: React.FC = ({ cert }) => { + const ShaComponent = ({ text, val }: { text: string; val: string }) => { + return ( + + + {text} + + + {copy => } + + + ); + }; + return ( + <> + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/certificates/index.ts b/x-pack/plugins/uptime/public/components/certificates/index.ts new file mode 100644 index 0000000000000..82f3f7ab67c91 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './cert_monitors'; +export * from './cert_search'; +export * from './cert_status'; +export * from './certificates_list'; +export * from './fingerprint_col'; diff --git a/x-pack/plugins/uptime/public/components/certificates/translations.ts b/x-pack/plugins/uptime/public/components/certificates/translations.ts new file mode 100644 index 0000000000000..518eddf1211a4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/translations.ts @@ -0,0 +1,63 @@ +/* + * 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 OK = i18n.translate('xpack.uptime.certs.ok', { + defaultMessage: 'OK', +}); + +export const EXPIRED = i18n.translate('xpack.uptime.certs.expired', { + defaultMessage: 'Expired', +}); + +export const EXPIRES_SOON = i18n.translate('xpack.uptime.certs.expireSoon', { + defaultMessage: 'Expires soon', +}); + +export const SEARCH_CERTS = i18n.translate('xpack.uptime.certs.searchCerts', { + defaultMessage: 'Search certificates', +}); + +export const STATUS_COL = i18n.translate('xpack.uptime.certs.list.status', { + defaultMessage: 'Status', +}); + +export const TOO_OLD = i18n.translate('xpack.uptime.certs.list.status.old', { + defaultMessage: 'Too old', +}); + +export const COMMON_NAME_COL = i18n.translate('xpack.uptime.certs.list.commonName', { + defaultMessage: 'Common name', +}); + +export const MONITORS_COL = i18n.translate('xpack.uptime.certs.list.monitors', { + defaultMessage: 'Monitors', +}); + +export const ISSUED_BY_COL = i18n.translate('xpack.uptime.certs.list.issuedBy', { + defaultMessage: 'Issued by', +}); + +export const VALID_UNTIL_COL = i18n.translate('xpack.uptime.certs.list.validUntil', { + defaultMessage: 'Valid until', +}); + +export const AGE_COL = i18n.translate('xpack.uptime.certs.list.ageCol', { + defaultMessage: 'Age', +}); + +export const DAYS = i18n.translate('xpack.uptime.certs.list.days', { + defaultMessage: 'days', +}); + +export const FINGERPRINTS_COL = i18n.translate('xpack.uptime.certs.list.expirationDate', { + defaultMessage: 'Fingerprints', +}); + +export const COPY_FINGERPRINT = i18n.translate('xpack.uptime.certs.list.copyFingerprint', { + defaultMessage: 'Click to copy fingerprint value', +}); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/monitor_page_link.test.tsx.snap similarity index 100% rename from x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/monitor_page_link.test.tsx.snap diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_page_link.test.tsx b/x-pack/plugins/uptime/public/components/common/__tests__/monitor_page_link.test.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_page_link.test.tsx rename to x-pack/plugins/uptime/public/components/common/__tests__/monitor_page_link.test.tsx index dd6e9c66d395b..36ebeb6615648 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_page_link.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/__tests__/monitor_page_link.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorPageLink } from '../monitor_page_link'; describe('MonitorPageLink component', () => { diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx b/x-pack/plugins/uptime/public/components/common/monitor_page_link.tsx similarity index 89% rename from x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx rename to x-pack/plugins/uptime/public/components/common/monitor_page_link.tsx index 803b399810508..77faa8edfc5c8 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx +++ b/x-pack/plugins/uptime/public/components/common/monitor_page_link.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { FC } from 'react'; import { EuiLink } from '@elastic/eui'; import { Link } from 'react-router-dom'; -import React, { FunctionComponent } from 'react'; interface DetailPageLinkProps { /** @@ -19,7 +19,7 @@ interface DetailPageLinkProps { linkParameters: string | undefined; } -export const MonitorPageLink: FunctionComponent = ({ +export const MonitorPageLink: FC = ({ children, monitorId, linkParameters, diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap deleted file mode 100644 index 605fc3cdb6b38..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MonitorStatusBar component renders 1`] = ` -Array [ -
    , -
    - SSL certificate expires - - - - in 2 months - - - -
    , -] -`; - -exports[`MonitorStatusBar component renders null if invalid date 1`] = `null`; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap new file mode 100644 index 0000000000000..2f4473ba54cf9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap @@ -0,0 +1,119 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SSL Certificate component renders 1`] = ` +Array [ +
    + Certificate +
    , +
    , +
    +
    +
    + Expires + + + + in 2 months + + + +
    +
    + +
    , +] +`; + +exports[`SSL Certificate component renders null if invalid date 1`] = `null`; + +exports[`SSL Certificate component shallow renders 1`] = ` + + + +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx index 5fd32c808da42..b39a1cb537583 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorStatusBarComponent } from '../monitor_status_bar'; import { Ping } from '../../../../../common/runtime_types'; +import * as redux from 'react-redux'; describe('MonitorStatusBar component', () => { let monitorStatus: Ping; @@ -46,6 +47,12 @@ describe('MonitorStatusBar component', () => { }, ], }; + + const spy = jest.spyOn(redux, 'useDispatch'); + spy.mockReturnValue(jest.fn()); + + const spy1 = jest.spyOn(redux, 'useSelector'); + spy1.mockReturnValue(true); }); it('renders duration in ms, not us', () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/ssl_certificate.test.tsx similarity index 55% rename from x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/ssl_certificate.test.tsx index 57ed09cc30ef1..70a161a2394ec 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/ssl_certificate.test.tsx @@ -6,13 +6,14 @@ import React from 'react'; import moment from 'moment'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { EuiBadge } from '@elastic/eui'; -import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { Tls } from '../../../../../common/runtime_types'; import { MonitorSSLCertificate } from '../monitor_status_bar'; +import * as redux from 'react-redux'; +import { mountWithRouter, renderWithRouter, shallowWithRouter } from '../../../../lib'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../common/constants'; -describe('MonitorStatusBar component', () => { +describe('SSL Certificate component', () => { let monitorTls: Tls; beforeEach(() => { @@ -21,37 +22,52 @@ describe('MonitorStatusBar component', () => { .toString(); monitorTls = { - certificate_not_valid_after: dateInTwoMonths, + not_after: dateInTwoMonths, }; + + const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); + useDispatchSpy.mockReturnValue(jest.fn()); + + const useSelectorSpy = jest.spyOn(redux, 'useSelector'); + useSelectorSpy.mockReturnValue({ settings: DYNAMIC_SETTINGS_DEFAULTS }); + }); + + it('shallow renders', () => { + const monitorTls1 = { + not_after: '2020-04-24T11:41:38.200Z', + }; + const component = shallowWithRouter(); + expect(component).toMatchSnapshot(); }); it('renders', () => { - const component = renderWithIntl(); + const component = renderWithRouter(); expect(component).toMatchSnapshot(); }); it('renders null if invalid date', () => { monitorTls = { - certificate_not_valid_after: 'i am so invalid date', + not_after: 'i am so invalid date', }; - const component = renderWithIntl(); + const component = renderWithRouter(); expect(component).toMatchSnapshot(); }); - it('renders expiration date with a warning state if ssl expiry date is less than 30 days', () => { - const dateIn15Days = moment() - .add(15, 'day') + it('renders expiration date with a warning state if ssl expiry date is less than 5 days', () => { + const dateIn5Days = moment() + .add(5, 'day') .toString(); monitorTls = { - certificate_not_valid_after: dateIn15Days, + not_after: dateIn5Days, }; - const component = mountWithIntl(); + const component = mountWithRouter(); const badgeComponent = component.find(EuiBadge); + expect(badgeComponent.props().color).toBe('warning'); const badgeComponentText = component.find('.euiBadge__text'); - expect(badgeComponentText.text()).toBe(moment(dateIn15Days).fromNow()); + expect(badgeComponentText.text()).toBe(moment(dateIn5Days).fromNow()); expect(badgeComponent.find('span.euiBadge--warning')).toBeTruthy(); }); @@ -61,9 +77,9 @@ describe('MonitorStatusBar component', () => { .add(40, 'day') .toString(); monitorTls = { - certificate_not_valid_after: dateIn40Days, + not_after: dateIn40Days, }; - const component = mountWithIntl(); + const component = mountWithRouter(); const badgeComponent = component.find(EuiBadge); expect(badgeComponent.props().color).toBe('default'); diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx index d92534aecd175..734a68f00f7de 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx @@ -6,10 +6,13 @@ import React from 'react'; import moment from 'moment'; -import { EuiSpacer, EuiText, EuiBadge } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { Link } from 'react-router-dom'; +import { EuiSpacer, EuiText, EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { Tls } from '../../../../../common/runtime_types'; +import { useCertStatus } from '../../../../hooks'; +import { CERT_STATUS, CERTIFICATES_ROUTE } from '../../../../../common/constants'; interface Props { /** @@ -19,40 +22,79 @@ interface Props { } export const MonitorSSLCertificate = ({ tls }: Props) => { - const certValidityDate = new Date(tls?.certificate_not_valid_after ?? ''); + const certStatus = useCertStatus(tls?.not_after); - const isValidDate = !isNaN(certValidityDate.valueOf()); + const isExpiringSoon = certStatus === CERT_STATUS.EXPIRING_SOON; - const dateIn30Days = moment().add('30', 'days'); + const isExpired = certStatus === CERT_STATUS.EXPIRED; - const isExpiringInMonth = isValidDate && dateIn30Days > moment(certValidityDate); + const relativeDate = moment(tls?.not_after).fromNow(); - return isValidDate ? ( + return certStatus ? ( <> - - - - {moment(certValidityDate).fromNow()} - - ), - }} - /> + + {i18n.translate('xpack.uptime.monitorStatusBar.sslCertificate.title', { + defaultMessage: 'Certificate', + })} + + + + + {isExpired ? ( + {relativeDate}, + }} + /> + ) : ( + + {relativeDate} + + ), + }} + /> + )} + + + + + + {i18n.translate('xpack.uptime.monitorStatusBar.sslCertificate.overview', { + defaultMessage: 'Certificate overview', + })} + + + + ) : null; }; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index ed5602323d254..0d6638e7070d6 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -556,13 +556,35 @@ exports[`MonitorList component renders the monitor list 1`] = `
    -
    - Monitor status -
    +
    +
    + Monitor status +
    +
    + +
    + +
    + + TLS Certificate + +
    + @@ -657,7 +695,7 @@ exports[`MonitorList component renders the monitor list 1`] = `
    + +
    + TLS Certificate +
    +
    + + - + +
    + @@ -951,6 +1005,22 @@ exports[`MonitorList component renders the monitor list 1`] = `
    + +
    + TLS Certificate +
    +
    + + - + +
    + diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx index 9b1d799a23e37..9dd44f5176664 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx @@ -12,12 +12,19 @@ import { } from '../../../../../common/runtime_types'; import { MonitorListComponent } from '../monitor_list'; import { renderWithRouter, shallowWithRouter } from '../../../../lib'; +import * as redux from 'react-redux'; describe('MonitorList component', () => { let result: MonitorSummaryResult; let localStorageMock: any; beforeEach(() => { + const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); + useDispatchSpy.mockReturnValue(jest.fn()); + + const useSelectorSpy = jest.spyOn(redux, 'useSelector'); + useSelectorSpy.mockReturnValue(true); + localStorageMock = { getItem: jest.fn().mockImplementation(() => '25'), setItem: jest.fn(), diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx new file mode 100644 index 0000000000000..d9380476eaf45 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx @@ -0,0 +1,51 @@ +/* + * 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 moment from 'moment'; +import styled from 'styled-components'; +import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; +import { Cert } from '../../../../common/runtime_types'; +import { useCertStatus } from '../../../hooks'; +import { EXPIRED, EXPIRES_SOON } from '../../certificates/translations'; +import { CERT_STATUS } from '../../../../common/constants'; + +interface Props { + cert: Cert; +} + +const Span = styled.span` + margin-left: 5px; + vertical-align: middle; +`; + +export const CertStatusColumn: React.FC = ({ cert }) => { + const certStatus = useCertStatus(cert?.not_after); + + const relativeDate = moment(cert?.not_after).fromNow(); + + const CertStatus = ({ color, text }: { color: string; text: string }) => { + return ( + + + + + {text} {relativeDate} + + + + ); + }; + + if (certStatus === CERT_STATUS.EXPIRING_SOON) { + return ; + } + if (certStatus === CERT_STATUS.EXPIRED) { + return ; + } + + return certStatus ? : -; +}; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx index 7e9536689470e..616d8fbd76043 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx @@ -18,12 +18,13 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; +import { Link } from 'react-router-dom'; import { HistogramPoint, FetchMonitorStatesQueryArgs } from '../../../../common/runtime_types'; import { MonitorSummary } from '../../../../common/runtime_types'; import { MonitorListStatusColumn } from './monitor_list_status_column'; import { ExpandedRowMap } from './types'; import { MonitorBarSeries } from '../../common/charts'; -import { MonitorPageLink } from './monitor_page_link'; +import { MonitorPageLink } from '../../common/monitor_page_link'; import { OverviewPageLink } from './overview_page_link'; import * as labels from './translations'; import { MonitorListPageSizeSelect } from './monitor_list_page_size_select'; @@ -31,6 +32,8 @@ import { MonitorListDrawer } from './monitor_list_drawer/list_drawer_container'; import { MonitorListProps } from './monitor_list_container'; import { MonitorList } from '../../../state/reducers/monitor_list'; import { useUrlParams } from '../../../hooks'; +import { CERTIFICATES_ROUTE } from '../../../../common/constants'; +import { CertStatusColumn } from './cert_status_column'; interface Props extends MonitorListProps { lastRefresh: number; @@ -143,6 +146,12 @@ export const MonitorListComponent: React.FC = ({ ), }, + { + align: 'center' as const, + field: 'state.tls', + name: labels.TLS_COLUMN_LABEL, + render: (tls: any) => , + }, { align: 'center' as const, field: 'histogram.points', @@ -181,15 +190,32 @@ export const MonitorListComponent: React.FC = ({ return ( - -
    - -
    -
    - + + + +
    + +
    +
    +
    + + +
    + + + +
    +
    +
    +
    + + { return i18n.translate('xpack.uptime.monitorList.expandDrawerButton.ariaLabel', { defaultMessage: 'Expand row for monitor with ID {id}', diff --git a/x-pack/plugins/uptime/public/hooks/index.ts b/x-pack/plugins/uptime/public/hooks/index.ts index 1f50e995eda49..b92d2d4cf7df5 100644 --- a/x-pack/plugins/uptime/public/hooks/index.ts +++ b/x-pack/plugins/uptime/public/hooks/index.ts @@ -8,3 +8,4 @@ export * from './use_monitor'; export * from './use_url_params'; export * from './use_telemetry'; export * from './update_kuery_string'; +export * from './use_cert_status'; diff --git a/x-pack/plugins/uptime/public/hooks/use_cert_status.ts b/x-pack/plugins/uptime/public/hooks/use_cert_status.ts new file mode 100644 index 0000000000000..cb54b05af9dd1 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_cert_status.ts @@ -0,0 +1,42 @@ +/* + * 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 moment from 'moment'; +import { useSelector } from 'react-redux'; +import { selectDynamicSettings } from '../state/selectors'; +import { CERT_STATUS } from '../../common/constants'; + +export const useCertStatus = (expiryDate?: string, issueDate?: string) => { + const dss = useSelector(selectDynamicSettings); + + const expiryThreshold = dss.settings?.certThresholds?.expiration; + + const ageThreshold = dss.settings?.certThresholds?.age; + + const certValidityDate = new Date(expiryDate ?? ''); + + const isValidDate = !isNaN(certValidityDate.valueOf()); + + if (!isValidDate) { + return false; + } + + const isExpiringSoon = moment(certValidityDate).diff(moment(), 'days') < expiryThreshold!; + + const isTooOld = moment().diff(moment(issueDate), 'days') > ageThreshold!; + + const isExpired = moment(certValidityDate) < moment(); + + if (isExpired) { + return CERT_STATUS.EXPIRED; + } + + return isExpiringSoon + ? CERT_STATUS.EXPIRING_SOON + : isTooOld + ? CERT_STATUS.TOO_OLD + : CERT_STATUS.OK; +}; diff --git a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts index a2012b8ac5636..9b4a441fe5ade 100644 --- a/x-pack/plugins/uptime/public/hooks/use_telemetry.ts +++ b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts @@ -13,6 +13,7 @@ export enum UptimePage { Overview = 'Overview', Monitor = 'Monitor', Settings = 'Settings', + Certificates = 'Certificates', NotFound = '__not-found__', } diff --git a/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/certificates.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/certificates.test.tsx.snap new file mode 100644 index 0000000000000..53b2ea27864bc --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/certificates.test.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertificatesPage shallow renders expected elements for valid props 1`] = ` + + + +`; diff --git a/x-pack/plugins/uptime/public/pages/__tests__/certificates.test.tsx b/x-pack/plugins/uptime/public/pages/__tests__/certificates.test.tsx new file mode 100644 index 0000000000000..8dfb6fba3d6be --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/__tests__/certificates.test.tsx @@ -0,0 +1,15 @@ +/* + * 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 { shallowWithRouter } from '../../lib'; +import { CertificatesPage } from '../certificates'; + +describe('CertificatesPage', () => { + it('shallow renders expected elements for valid props', () => { + expect(shallowWithRouter()).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/pages/certificates.tsx b/x-pack/plugins/uptime/public/pages/certificates.tsx new file mode 100644 index 0000000000000..d6c1b8e2b4568 --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/certificates.tsx @@ -0,0 +1,136 @@ +/* + * 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 { useDispatch, useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, +} from '@elastic/eui'; +import React, { useContext, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useTrackPageview } from '../../../observability/public'; +import { PageHeader } from './page_header'; +import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../../common/constants'; +import { getDynamicSettings } from '../state/actions/dynamic_settings'; +import { UptimeRefreshContext } from '../contexts'; +import * as labels from './translations'; +import { UptimePage, useUptimeTelemetry } from '../hooks'; +import { certificatesSelector, getCertificatesAction } from '../state/certificates/certificates'; +import { CertificateList, CertificateSearch, CertSort } from '../components/certificates'; + +const DEFAULT_PAGE_SIZE = 10; +const LOCAL_STORAGE_KEY = 'xpack.uptime.certList.pageSize'; +const getPageSizeValue = () => { + const value = parseInt(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '', 10); + if (isNaN(value)) { + return DEFAULT_PAGE_SIZE; + } + return value; +}; + +export const CertificatesPage: React.FC = () => { + useUptimeTelemetry(UptimePage.Certificates); + + useTrackPageview({ app: 'uptime', path: 'certificates' }); + useTrackPageview({ app: 'uptime', path: 'certificates', delay: 15000 }); + + useBreadcrumbs([{ text: 'Certificates' }]); + + const [page, setPage] = useState({ index: 0, size: getPageSizeValue() }); + const [sort, setSort] = useState({ + field: 'not_after', + direction: 'asc', + }); + const [search, setSearch] = useState(''); + + const dispatch = useDispatch(); + + const { lastRefresh, refreshApp } = useContext(UptimeRefreshContext); + + useEffect(() => { + dispatch(getDynamicSettings()); + }, [dispatch]); + + useEffect(() => { + dispatch( + getCertificatesAction.get({ + search, + ...page, + sortBy: sort.field, + direction: sort.direction, + }) + ); + }, [dispatch, page, search, sort.direction, sort.field, lastRefresh]); + + const certificates = useSelector(certificatesSelector); + + return ( + <> + + + + + {labels.RETURN_TO_OVERVIEW} + + + + + + + {labels.SETTINGS_ON_CERT} + + + + + { + refreshApp(); + }} + data-test-subj="superDatePickerApplyTimeButton" + > + {labels.REFRESH_CERT} + + + + + + + {certificates?.total ?? 0}, + }} + /> + } + datePicker={false} + /> + + + + { + setPage(pageVal); + setSort(sortVal); + localStorage.setItem(LOCAL_STORAGE_KEY, pageVal.size.toString()); + }} + sort={sort} + /> + + + ); +}; diff --git a/x-pack/plugins/uptime/public/pages/monitor.tsx b/x-pack/plugins/uptime/public/pages/monitor.tsx index 8a309db75acd2..fc796e679a2f6 100644 --- a/x-pack/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor.tsx @@ -5,8 +5,8 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React from 'react'; -import { useSelector } from 'react-redux'; +import React, { useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import { monitorStatusSelector } from '../state/selectors'; import { PageHeader } from './page_header'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; @@ -14,8 +14,15 @@ import { useTrackPageview } from '../../../observability/public'; import { useMonitorId, useUptimeTelemetry, UptimePage } from '../hooks'; import { MonitorCharts } from '../components/monitor'; import { MonitorStatusDetails, PingList } from '../components/monitor'; +import { getDynamicSettings } from '../state/actions/dynamic_settings'; export const MonitorPage: React.FC = () => { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(getDynamicSettings()); + }, [dispatch]); + const monitorId = useMonitorId(); const selectedMonitor = useSelector(monitorStatusSelector); diff --git a/x-pack/plugins/uptime/public/pages/page_header.tsx b/x-pack/plugins/uptime/public/pages/page_header.tsx index b10bc6ba44f8a..b6791e6a93445 100644 --- a/x-pack/plugins/uptime/public/pages/page_header.tsx +++ b/x-pack/plugins/uptime/public/pages/page_header.tsx @@ -13,7 +13,7 @@ import { SETTINGS_ROUTE } from '../../common/constants'; import { ToggleAlertFlyoutButton } from '../components/overview/alerts/alerts_containers'; interface PageHeaderProps { - headingText: string; + headingText: string | JSX.Element; extraLinks?: boolean; datePicker?: boolean; } diff --git a/x-pack/plugins/uptime/public/pages/translations.ts b/x-pack/plugins/uptime/public/pages/translations.ts index 85e4e3f931c46..74fb2eeb1416b 100644 --- a/x-pack/plugins/uptime/public/pages/translations.ts +++ b/x-pack/plugins/uptime/public/pages/translations.ts @@ -6,6 +6,21 @@ import { i18n } from '@kbn/i18n'; +export const SETTINGS_ON_CERT = i18n.translate('xpack.uptime.certificates.settingsLinkLabel', { + defaultMessage: 'Settings', +}); + +export const RETURN_TO_OVERVIEW = i18n.translate( + 'xpack.uptime.certificates.returnToOverviewLinkLabel', + { + defaultMessage: 'Return to overview', + } +); + +export const REFRESH_CERT = i18n.translate('xpack.uptime.certificates.refresh', { + defaultMessage: 'Refresh', +}); + export const settings = { breadcrumbText: i18n.translate('xpack.uptime.settingsBreadcrumbText', { defaultMessage: 'Settings', diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index eb0587c0417a2..ca97858998df7 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -8,8 +8,14 @@ import React, { FC } from 'react'; import { Route, Switch } from 'react-router-dom'; import { DataPublicPluginSetup } from '../../../../src/plugins/data/public'; import { OverviewPage } from './components/overview/overview_container'; -import { MONITOR_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../common/constants'; +import { + CERTIFICATES_ROUTE, + MONITOR_ROUTE, + OVERVIEW_ROUTE, + SETTINGS_ROUTE, +} from '../common/constants'; import { MonitorPage, NotFoundPage, SettingsPage } from './pages'; +import { CertificatesPage } from './pages/certificates'; interface RouterProps { autocomplete: DataPublicPluginSetup['autocomplete']; @@ -27,6 +33,11 @@ export const PageRouter: FC = ({ autocomplete }) => (
    + +
    + +
    +
    diff --git a/x-pack/plugins/uptime/public/state/api/certificates.ts b/x-pack/plugins/uptime/public/state/api/certificates.ts new file mode 100644 index 0000000000000..78267e659d233 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/certificates.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 { API_URLS } from '../../../common/constants'; +import { apiService } from './utils'; +import { CertResultType, GetCertsParams } from '../../../common/runtime_types'; + +export const fetchCertificates = async (params: GetCertsParams) => { + return await apiService.get(API_URLS.CERTS, params, CertResultType); +}; diff --git a/x-pack/plugins/uptime/public/state/certificates/certificates.ts b/x-pack/plugins/uptime/public/state/certificates/certificates.ts new file mode 100644 index 0000000000000..18cbcf6bcb614 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/certificates/certificates.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 { handleActions } from 'redux-actions'; +import { takeLatest } from 'redux-saga/effects'; +import { createAsyncAction } from '../actions/utils'; +import { getAsyncInitialState, handleAsyncAction } from '../reducers/utils'; +import { CertResult, GetCertsParams } from '../../../common/runtime_types'; +import { AppState } from '../index'; +import { AsyncInitialState } from '../reducers/types'; +import { fetchEffectFactory } from '../effects/fetch_effect'; +import { fetchCertificates } from '../api/certificates'; + +export const getCertificatesAction = createAsyncAction( + 'GET_CERTIFICATES' +); + +interface CertificatesState { + certs: AsyncInitialState; +} + +const initialState = { + certs: getAsyncInitialState(), +}; + +export const certificatesReducer = handleActions( + { + ...handleAsyncAction('certs', getCertificatesAction), + }, + initialState +); + +export function* fetchCertificatesEffect() { + yield takeLatest( + getCertificatesAction.get, + fetchEffectFactory(fetchCertificates, getCertificatesAction.success, getCertificatesAction.fail) + ); +} + +export const certificatesSelector = ({ certificates }: AppState) => certificates.certs.data; diff --git a/x-pack/plugins/uptime/public/state/effects/index.ts b/x-pack/plugins/uptime/public/state/effects/index.ts index 739179c5bbeae..211067c840d54 100644 --- a/x-pack/plugins/uptime/public/state/effects/index.ts +++ b/x-pack/plugins/uptime/public/state/effects/index.ts @@ -16,6 +16,7 @@ import { fetchPingsEffect, fetchPingHistogramEffect } from './ping'; import { fetchMonitorDurationEffect } from './monitor_duration'; import { fetchMLJobEffect } from './ml_anomaly'; import { fetchIndexStatusEffect } from './index_status'; +import { fetchCertificatesEffect } from '../certificates/certificates'; export function* rootEffect() { yield fork(fetchMonitorDetailsEffect); @@ -31,4 +32,5 @@ export function* rootEffect() { yield fork(fetchMLJobEffect); yield fork(fetchMonitorDurationEffect); yield fork(fetchIndexStatusEffect); + yield fork(fetchCertificatesEffect); } diff --git a/x-pack/plugins/uptime/public/state/reducers/index.ts b/x-pack/plugins/uptime/public/state/reducers/index.ts index 294bde2f277ec..ead7f5b46431b 100644 --- a/x-pack/plugins/uptime/public/state/reducers/index.ts +++ b/x-pack/plugins/uptime/public/state/reducers/index.ts @@ -18,6 +18,7 @@ import { pingListReducer } from './ping_list'; import { monitorDurationReducer } from './monitor_duration'; import { indexStatusReducer } from './index_status'; import { mlJobsReducer } from './ml_anomaly'; +import { certificatesReducer } from '../certificates/certificates'; export const rootReducer = combineReducers({ monitor: monitorReducer, @@ -33,4 +34,5 @@ export const rootReducer = combineReducers({ ml: mlJobsReducer, monitorDuration: monitorDurationReducer, indexStatus: indexStatusReducer, + certificates: certificatesReducer, }); diff --git a/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts index 61e03a9592921..9a4a949ac4ede 100644 --- a/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts +++ b/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts @@ -15,7 +15,6 @@ import { getMLCapabilitiesAction, } from '../actions'; import { getAsyncInitialState, handleAsyncAction } from './utils'; -import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; import { AsyncInitialState } from './types'; import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; import { CreateMLJobSuccess, DeleteJobResults } from '../actions/types'; @@ -37,15 +36,13 @@ const initialState: MLJobState = { mlCapabilities: getAsyncInitialState(), }; -type Payload = IHttpFetchError; - export const mlJobsReducer = handleActions( { - ...handleAsyncAction('mlJob', getExistingMLJobAction), - ...handleAsyncAction('mlCapabilities', getMLCapabilitiesAction), - ...handleAsyncAction('createJob', createMLJobAction), - ...handleAsyncAction('deleteJob', deleteMLJobAction), - ...handleAsyncAction('anomalies', getAnomalyRecordsAction), + ...handleAsyncAction('mlJob', getExistingMLJobAction), + ...handleAsyncAction('mlCapabilities', getMLCapabilitiesAction), + ...handleAsyncAction('createJob', createMLJobAction), + ...handleAsyncAction('deleteJob', deleteMLJobAction), + ...handleAsyncAction('anomalies', getAnomalyRecordsAction), ...{ [String(resetMLState)]: state => ({ ...state, diff --git a/x-pack/plugins/uptime/public/state/reducers/monitor_list.ts b/x-pack/plugins/uptime/public/state/reducers/monitor_list.ts index cf895aebeb755..59a794a549d57 100644 --- a/x-pack/plugins/uptime/public/state/reducers/monitor_list.ts +++ b/x-pack/plugins/uptime/public/state/reducers/monitor_list.ts @@ -9,9 +9,9 @@ import { getMonitorList, getMonitorListSuccess, getMonitorListFailure } from '.. import { MonitorSummaryResult } from '../../../common/runtime_types'; export interface MonitorList { - list: MonitorSummaryResult; error?: Error; loading: boolean; + list: MonitorSummaryResult; } export const initialState: MonitorList = { diff --git a/x-pack/plugins/uptime/public/state/reducers/utils.ts b/x-pack/plugins/uptime/public/state/reducers/utils.ts index d7a7f237c1154..15e49e7f6de8b 100644 --- a/x-pack/plugins/uptime/public/state/reducers/utils.ts +++ b/x-pack/plugins/uptime/public/state/reducers/utils.ts @@ -7,7 +7,7 @@ import { Action } from 'redux-actions'; import { AsyncAction } from '../actions/types'; -export function handleAsyncAction( +export function handleAsyncAction( storeKey: string, asyncAction: AsyncAction ) { @@ -24,7 +24,7 @@ export function handleAsyncAction( ...state, [storeKey]: { ...(state as any)[storeKey], - data: action.payload === null ? action.payload : { ...action.payload }, + data: action.payload, loading: false, }, }), diff --git a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts index ba5e5abf588b8..1c4c12f5f52d2 100644 --- a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -101,6 +101,12 @@ describe('state selectors', () => { loading: false, }, }, + certificates: { + certs: { + data: null, + loading: false, + }, + }, }; it('selects base path from state', () => { diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts index 894e2316dc927..4aec376ceadf0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts @@ -19,9 +19,10 @@ describe('getCerts', () => { _score: 0, _source: { tls: { - certificate_not_valid_before: '2019-08-16T01:40:25.000Z', server: { x509: { + not_before: '2019-08-16T01:40:25.000Z', + not_after: '2020-07-16T03:15:39.000Z', subject: { common_name: 'r2.shared.global.fastly.net', }, @@ -34,12 +35,14 @@ describe('getCerts', () => { sha256: '12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d', }, }, - certificate_not_valid_after: '2020-07-16T03:15:39.000Z', }, monitor: { name: 'Real World Test', id: 'real-world-test', }, + url: { + full: 'https://fullurl.com', + }, }, fields: { 'tls.server.hash.sha256': [ @@ -96,24 +99,30 @@ describe('getCerts', () => { to: 'now+1h', search: 'my_common_name', size: 30, + sortBy: 'not_after', + direction: 'desc', }); expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "certificate_not_valid_after": "2020-07-16T03:15:39.000Z", - "certificate_not_valid_before": "2019-08-16T01:40:25.000Z", - "common_name": "r2.shared.global.fastly.net", - "issuer": "GlobalSign CloudSSL CA - SHA256 - G3", - "monitors": Array [ - Object { - "id": "real-world-test", - "name": "Real World Test", - }, - ], - "sha1": "b7b4b89ef0d0caf39d223736f0fdbb03c7b426f1", - "sha256": "12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d", - }, - ] + Object { + "certs": Array [ + Object { + "common_name": "r2.shared.global.fastly.net", + "issuer": "GlobalSign CloudSSL CA - SHA256 - G3", + "monitors": Array [ + Object { + "id": "real-world-test", + "name": "Real World Test", + "url": undefined, + }, + ], + "not_after": "2020-07-16T03:15:39.000Z", + "not_before": "2019-08-16T01:40:25.000Z", + "sha1": "b7b4b89ef0d0caf39d223736f0fdbb03c7b426f1", + "sha256": "12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d", + }, + ], + "total": 0, + } `); expect(mockCallES.mock.calls).toMatchInlineSnapshot(` Array [ @@ -128,9 +137,16 @@ describe('getCerts', () => { "tls.server.x509.subject.common_name", "tls.server.hash.sha1", "tls.server.hash.sha256", - "tls.certificate_not_valid_before", - "tls.certificate_not_valid_after", + "tls.server.x509.not_after", + "tls.server.x509.not_before", ], + "aggs": Object { + "total": Object { + "cardinality": Object { + "field": "tls.server.hash.sha256", + }, + }, + }, "collapse": Object { "field": "tls.server.hash.sha256", "inner_hits": Object { @@ -138,6 +154,7 @@ describe('getCerts', () => { "includes": Array [ "monitor.id", "monitor.name", + "url.full", ], }, "collapse": Object { @@ -151,13 +168,13 @@ describe('getCerts', () => { ], }, }, - "from": 1, + "from": 30, "query": Object { "bool": Object { "filter": Array [ Object { "exists": Object { - "field": "tls", + "field": "tls.server", }, }, Object { @@ -169,39 +186,32 @@ describe('getCerts', () => { }, }, ], + "minimum_should_match": 1, "should": Array [ Object { - "wildcard": Object { - "tls.server.issuer": Object { - "value": "*my_common_name*", - }, - }, - }, - Object { - "wildcard": Object { - "tls.common_name": Object { - "value": "*my_common_name*", - }, - }, - }, - Object { - "wildcard": Object { - "monitor.id": Object { - "value": "*my_common_name*", - }, - }, - }, - Object { - "wildcard": Object { - "monitor.name": Object { - "value": "*my_common_name*", - }, + "multi_match": Object { + "fields": Array [ + "monitor.id.text", + "monitor.name.text", + "url.full.text", + "tls.server.x509.subject.common_name.text", + "tls.server.x509.issuer.common_name.text", + ], + "query": "my_common_name", + "type": "phrase_prefix", }, }, ], }, }, "size": 30, + "sort": Array [ + Object { + "tls.server.x509.not_after": Object { + "order": "desc", + }, + }, + ], }, "index": "heartbeat*", }, diff --git a/x-pack/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 index 75bf5096bd997..f8a335c387f2e 100644 --- a/x-pack/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 @@ -32,7 +32,14 @@ describe('getLatestMonitor', () => { }, }, size: 1, - _source: ['url', 'monitor', 'observer', 'tls', '@timestamp'], + _source: [ + 'url', + 'monitor', + 'observer', + '@timestamp', + 'tls.server.x509.not_after', + 'tls.server.x509.not_before', + ], sort: { '@timestamp': { order: 'desc' }, }, @@ -83,6 +90,10 @@ describe('getLatestMonitor', () => { "type": "http", }, "timestamp": "123456", + "tls": Object { + "not_after": undefined, + "not_before": undefined, + }, } `); expect(result.timestamp).toBe('123456'); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts index b427e7cae1a7e..6820cd69376d1 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts @@ -5,9 +5,16 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Cert, GetCertsParams } from '../../../common/runtime_types'; +import { CertResult, GetCertsParams } from '../../../common/runtime_types'; -export const getCerts: UMElasticsearchQueryFn = async ({ +enum SortFields { + 'issuer' = 'tls.server.x509.issuer.common_name', + 'not_after' = 'tls.server.x509.not_after', + 'not_before' = 'tls.server.x509.not_before', + 'common_name' = 'tls.server.x509.subject.common_name', +} + +export const getCerts: UMElasticsearchQueryFn = async ({ callES, dynamicSettings, index, @@ -15,19 +22,29 @@ export const getCerts: UMElasticsearchQueryFn = async ({ to, search, size, + sortBy, + direction, }) => { - const searchWrapper = `*${search}*`; + const sort = SortFields[sortBy as keyof typeof SortFields]; + const params: any = { index: dynamicSettings.heartbeatIndices, body: { - from: index, + from: index * size, size, + sort: [ + { + [sort]: { + order: direction, + }, + }, + ], query: { bool: { filter: [ { exists: { - field: 'tls', + field: 'tls.server', }, }, { @@ -48,14 +65,14 @@ export const getCerts: UMElasticsearchQueryFn = async ({ 'tls.server.x509.subject.common_name', 'tls.server.hash.sha1', 'tls.server.hash.sha256', - 'tls.certificate_not_valid_before', - 'tls.certificate_not_valid_after', + 'tls.server.x509.not_after', + 'tls.server.x509.not_before', ], collapse: { field: 'tls.server.hash.sha256', inner_hits: { _source: { - includes: ['monitor.id', 'monitor.name'], + includes: ['monitor.id', 'monitor.name', 'url.full'], }, collapse: { field: 'monitor.id', @@ -64,72 +81,67 @@ export const getCerts: UMElasticsearchQueryFn = async ({ sort: [{ 'monitor.id': 'asc' }], }, }, + aggs: { + total: { + cardinality: { + field: 'tls.server.hash.sha256', + }, + }, + }, }, }; if (search) { + params.body.query.bool.minimum_should_match = 1; params.body.query.bool.should = [ { - wildcard: { - 'tls.server.issuer': { - value: searchWrapper, - }, - }, - }, - { - wildcard: { - 'tls.common_name': { - value: searchWrapper, - }, - }, - }, - { - wildcard: { - 'monitor.id': { - value: searchWrapper, - }, - }, - }, - { - wildcard: { - 'monitor.name': { - value: searchWrapper, - }, + multi_match: { + query: escape(search), + type: 'phrase_prefix', + fields: [ + 'monitor.id.text', + 'monitor.name.text', + 'url.full.text', + 'tls.server.x509.subject.common_name.text', + 'tls.server.x509.issuer.common_name.text', + ], }, }, ]; } const result = await callES('search', params); - const formatted = (result?.hits?.hits ?? []).map((hit: any) => { + + const certs = (result?.hits?.hits ?? []).map((hit: any) => { const { _source: { - tls: { - server: { - x509: { - issuer: { common_name: issuer }, - subject: { common_name }, - }, - hash: { sha1, sha256 }, - }, - certificate_not_valid_after, - certificate_not_valid_before, - }, + tls: { server }, }, } = hit; + + const notAfter = server?.x509?.not_after; + const notBefore = server?.x509?.not_before; + const issuer = server?.x509?.issuer?.common_name; + const commonName = server?.x509?.subject?.common_name; + const sha1 = server?.hash?.sha1; + const sha256 = server?.hash?.sha256; + const monitors = hit.inner_hits.monitors.hits.hits.map((monitor: any) => ({ name: monitor._source?.monitor.name, id: monitor._source?.monitor.id, + url: monitor._source?.url?.full, })); + return { monitors, - certificate_not_valid_after, - certificate_not_valid_before, issuer, sha1, sha256, - common_name, + not_after: notAfter, + not_before: notBefore, + common_name: commonName, }; }); - return formatted; + const total = result?.aggregations?.total?.value ?? 0; + return { certs, total }; }; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index 98ce449002f21..db34de5159213 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -45,7 +45,14 @@ export const getLatestMonitor: UMElasticsearchQueryFn = UMElasticsearchQueryFn; export interface UptimeRequests { - getCerts: ESQ; + getCerts: ESQ; getFilterBar: ESQ; getIndexPattern: ESQ<{}, {}>; getLatestMonitor: ESQ; diff --git a/x-pack/plugins/uptime/server/rest_api/certs.ts b/x-pack/plugins/uptime/server/rest_api/certs/certs.ts similarity index 63% rename from x-pack/plugins/uptime/server/rest_api/certs.ts rename to x-pack/plugins/uptime/server/rest_api/certs/certs.ts index f2e1700b23e7d..a5ca6e264d299 100644 --- a/x-pack/plugins/uptime/server/rest_api/certs.ts +++ b/x-pack/plugins/uptime/server/rest_api/certs/certs.ts @@ -5,14 +5,16 @@ */ import { schema } from '@kbn/config-schema'; -import { UMServerLibs } from '../lib/lib'; -import { UMRestApiRouteFactory } from '.'; -import { API_URLS } from '../../common/constants'; +import { API_URLS } from '../../../common/constants'; +import { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteFactory } from '../types'; const DEFAULT_INDEX = 0; const DEFAULT_SIZE = 25; const DEFAULT_FROM = 'now-1d'; const DEFAULT_TO = 'now'; +const DEFAULT_SORT = 'not_after'; +const DEFAULT_DIRECTION = 'asc'; export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', @@ -24,30 +26,33 @@ export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = search: schema.maybe(schema.string()), index: schema.maybe(schema.number()), size: schema.maybe(schema.number()), + sortBy: schema.maybe(schema.string()), + direction: schema.maybe(schema.string()), }), }, - writeAccess: false, - options: { - tags: ['access:uptime-read'], - }, handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { const index = request.query?.index ?? DEFAULT_INDEX; const size = request.query?.size ?? DEFAULT_SIZE; const from = request.query?.from ?? DEFAULT_FROM; const to = request.query?.to ?? DEFAULT_TO; + const sortBy = request.query?.sortBy ?? DEFAULT_SORT; + const direction = request.query?.direction ?? DEFAULT_DIRECTION; const { search } = request.query; - + const result = await libs.requests.getCerts({ + callES, + dynamicSettings, + index, + search, + size, + from, + to, + sortBy, + direction, + }); return response.ok({ body: { - certs: await libs.requests.getCerts({ - callES, - dynamicSettings, - index, - search, - size, - from, - to, - }), + certs: result.certs, + total: result.total, }, }); }, diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index a7a63342d11d4..2b598be284e1c 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createGetCertsRoute } from './certs'; +import { createGetCertsRoute } from './certs/certs'; import { createGetOverviewFilters } from './overview_filters'; import { createGetPingHistogramRoute, createGetPingsRoute } from './pings'; import { createGetDynamicSettingsRoute, createPostDynamicSettingsRoute } from './dynamic_settings'; @@ -19,6 +19,7 @@ import { } from './monitors'; import { createGetMonitorDurationRoute } from './monitors/monitors_durations'; import { createGetIndexPatternRoute, createGetIndexStatusRoute } from './index_state'; + export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; export { uptimeRouteWrapper } from './uptime_route_wrapper'; diff --git a/x-pack/test/api_integration/apis/uptime/rest/certs.ts b/x-pack/test/api_integration/apis/uptime/rest/certs.ts index a3a15d8f8b014..4917917fdd6bc 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/certs.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/certs.ts @@ -21,7 +21,7 @@ export default function({ getService }: FtrProviderContext) { describe('empty index', async () => { it('returns empty array for no data', async () => { const apiResponse = await supertest.get(API_URLS.CERTS); - expect(JSON.stringify(apiResponse.body)).to.eql('{"certs":[]}'); + expect(JSON.stringify(apiResponse.body)).to.eql('{"certs":[],"total":0}'); }); }); @@ -39,10 +39,10 @@ export default function({ getService }: FtrProviderContext) { 10000, { tls: { - certificate_not_valid_after: cnva, - certificate_not_valid_before: cnvb, server: { x509: { + not_after: cnva, + not_before: cnvb, issuer: { common_name: 'issuer-common-name', }, @@ -78,9 +78,12 @@ export default function({ getService }: FtrProviderContext) { const cert = body.certs[0]; expect(Array.isArray(cert.monitors)).to.be(true); - expect(cert.monitors[0]).to.eql({ id: monitorId }); - expect(cert.certificate_not_valid_after).to.eql(cnva); - expect(cert.certificate_not_valid_before).to.eql(cnvb); + expect(cert.monitors[0]).to.eql({ + id: monitorId, + url: 'http://localhost:5678/pattern?r=200x5,500x1', + }); + expect(cert.not_after).to.eql(cnva); + expect(cert.not_before).to.eql(cnvb); expect(cert.common_name).to.eql('subject-common-name'); expect(cert.issuer).to.eql('issuer-common-name'); }); diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json index 9a33be807670e..1baff443bd97f 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json @@ -27,5 +27,6 @@ "full": "http://localhost:5678/pattern?r=200x1" }, "docId": "h5toHm0B0I9WX_CznN_V", - "timestamp": "2019-09-11T03:40:34.371Z" -} \ No newline at end of file + "timestamp": "2019-09-11T03:40:34.371Z", + "tls": {} +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts index ae326c8b2aee0..5f62a3c55a2eb 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts @@ -6,119 +6,36 @@ import uuid from 'uuid'; import { merge, flattenDeep } from 'lodash'; - -const INDEX_NAME = 'heartbeat-8-generated-test'; - -export const makePing = async ( - es: any, - monitorId: string, - fields: { [key: string]: any }, - mogrify: (doc: any) => any, - refresh: boolean = true -) => { - const baseDoc = { - tcp: { - rtt: { - connect: { - us: 14687, - }, - }, - }, - observer: { - geo: { - name: 'mpls', - location: '37.926868, -78.024902', - }, - hostname: 'avc-x1e', - }, - agent: { - hostname: 'avc-x1e', - id: '10730a1a-4cb7-45ce-8524-80c4820476ab', - type: 'heartbeat', - ephemeral_id: '0d9a8dc6-f604-49e3-86a0-d8f9d6f2cbad', - version: '8.0.0', - }, - '@timestamp': new Date().toISOString(), - resolve: { - rtt: { - us: 350, - }, - ip: '127.0.0.1', - }, - ecs: { - version: '1.1.0', - }, - host: { - name: 'avc-x1e', - }, - http: { - rtt: { - response_header: { - us: 19349, - }, - total: { - us: 48954, - }, - write_request: { - us: 33, - }, - content: { - us: 51, - }, - validate: { - us: 19400, - }, - }, - response: { - status_code: 200, - body: { - bytes: 3, - hash: '27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf', - }, - }, - }, - monitor: { - duration: { - us: 49347, - }, - ip: '127.0.0.1', - id: monitorId, - check_group: uuid.v4(), - type: 'http', - status: 'up', - }, - event: { - dataset: 'uptime', - }, - url: { - path: '/pattern', - scheme: 'http', - port: 5678, - domain: 'localhost', - query: 'r=200x5,500x1', - full: 'http://localhost:5678/pattern?r=200x5,500x1', - }, - }; - - const doc = mogrify(merge(baseDoc, fields)); - - await es.index({ - index: INDEX_NAME, - refresh, - body: doc, - }); - - return doc; +import { makePing } from './make_ping'; +import { TlsProps } from './make_tls'; + +interface CheckProps { + es: any; + monitorId?: string; + numIps?: number; + fields?: { [key: string]: any }; + mogrify?: (doc: any) => any; + refresh?: boolean; + tls?: boolean | TlsProps; +} + +const getRandomMonitorId = () => { + return ( + 'monitor-' + + Math.random() + .toString(36) + .substring(7) + ); }; - -export const makeCheck = async ( - es: any, - monitorId: string, - numIps: number, - fields: { [key: string]: any }, - mogrify: (doc: any) => any, - refresh: boolean = true -) => { +export const makeCheck = async ({ + es, + monitorId = getRandomMonitorId(), + numIps = 1, + fields = {}, + mogrify = d => d, + refresh = true, + tls = false, +}: CheckProps): Promise<{ monitorId: string; docs: any }> => { const cgFields = { monitor: { check_group: uuid.v4(), @@ -139,7 +56,7 @@ export const makeCheck = async ( if (i === numIps - 1) { pingFields.summary = summary; } - const doc = await makePing(es, monitorId, pingFields, mogrify, false); + const doc = await makePing(es, monitorId, pingFields, mogrify, false, tls as any); docs.push(doc); // @ts-ignore summary[doc.monitor.status]++; @@ -149,15 +66,15 @@ export const makeCheck = async ( await es.indices.refresh(); } - return docs; + return { monitorId, docs }; }; export const makeChecks = async ( es: any, monitorId: string, - numChecks: number, - numIps: number, - every: number, // number of millis between checks + numChecks: number = 1, + numIps: number = 1, + every: number = 10000, // number of millis between checks fields: { [key: string]: any } = {}, mogrify: (doc: any) => any = d => d, refresh: boolean = true @@ -177,7 +94,8 @@ export const makeChecks = async ( }, }, }); - checks.push(await makeCheck(es, monitorId, numIps, fields, mogrify, false)); + const { docs } = await makeCheck({ es, monitorId, numIps, fields, mogrify, refresh: false }); + checks.push(docs); } if (refresh) { diff --git a/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts new file mode 100644 index 0000000000000..908c571e07e06 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts @@ -0,0 +1,118 @@ +/* + * 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 uuid from 'uuid'; +import { merge } from 'lodash'; +import { makeTls, TlsProps } from './make_tls'; + +const INDEX_NAME = 'heartbeat-8-generated-test'; + +export const makePing = async ( + es: any, + monitorId: string, + fields: { [key: string]: any }, + mogrify: (doc: any) => any, + refresh: boolean = true, + tls: boolean | TlsProps = false +) => { + const baseDoc: any = { + tcp: { + rtt: { + connect: { + us: 14687, + }, + }, + }, + observer: { + geo: { + name: 'mpls', + location: '37.926868, -78.024902', + }, + hostname: 'avc-x1e', + }, + agent: { + hostname: 'avc-x1e', + id: '10730a1a-4cb7-45ce-8524-80c4820476ab', + type: 'heartbeat', + ephemeral_id: '0d9a8dc6-f604-49e3-86a0-d8f9d6f2cbad', + version: '8.0.0', + }, + '@timestamp': new Date().toISOString(), + resolve: { + rtt: { + us: 350, + }, + ip: '127.0.0.1', + }, + ecs: { + version: '1.1.0', + }, + host: { + name: 'avc-x1e', + }, + http: { + rtt: { + response_header: { + us: 19349, + }, + total: { + us: 48954, + }, + write_request: { + us: 33, + }, + content: { + us: 51, + }, + validate: { + us: 19400, + }, + }, + response: { + status_code: 200, + body: { + bytes: 3, + hash: '27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf', + }, + }, + }, + monitor: { + duration: { + us: 49347, + }, + ip: '127.0.0.1', + id: monitorId, + check_group: uuid.v4(), + type: 'http', + status: 'up', + }, + event: { + dataset: 'uptime', + }, + url: { + path: '/pattern', + scheme: 'http', + port: 5678, + domain: 'localhost', + query: 'r=200x5,500x1', + full: 'http://localhost:5678/pattern?r=200x5,500x1', + }, + }; + + if (tls) { + baseDoc.tls = makeTls(tls as any); + } + + const doc = mogrify(merge(baseDoc, fields)); + + await es.index({ + index: INDEX_NAME, + refresh, + body: doc, + }); + + return doc; +}; diff --git a/x-pack/test/api_integration/apis/uptime/rest/helper/make_tls.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_tls.ts new file mode 100644 index 0000000000000..3606462522024 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/make_tls.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import crypto from 'crypto'; + +export interface TlsProps { + valid?: boolean; + commonName?: string; + expiry?: string; + sha256?: string; +} + +type Props = TlsProps & boolean; + +// Note This is just a mock sha256 value, this doesn't actually generate actually sha 256 val +export const getSha256 = () => { + return crypto + .randomBytes(64) + .toString('hex') + .toUpperCase(); +}; + +export const makeTls = ({ valid = true, commonName = '*.elastic.co', expiry, sha256 }: Props) => { + const expiryDate = + expiry ?? + moment() + .add(valid ? 2 : -2, 'months') + .toISOString(); + + return { + version: '1.3', + cipher: 'TLS-AES-128-GCM-SHA256', + certificate_not_valid_before: '2020-03-01T00:00:00.000Z', + certificate_not_valid_after: expiryDate, + server: { + x509: { + not_before: '2020-03-01T00:00:00.000Z', + not_after: '2020-05-30T12:00:00.000Z', + issuer: { + distinguished_name: + 'CN=DigiCert SHA2 High Assurance Server CA,OU=www.digicert.com,O=DigiCert Inc,C=US', + common_name: 'DigiCert SHA2 High Assurance Server CA', + }, + subject: { + common_name: commonName, + distinguished_name: 'CN=*.facebook.com,O=Facebook Inc.,L=Menlo Park,ST=California,C=US', + }, + serial_number: '10043199409725537507026285099403602396', + signature_algorithm: 'SHA256-RSA', + public_key_algorithm: 'ECDSA', + public_key_curve: 'P-256', + }, + hash: { + sha256: sha256 ?? '1a48f1db13c3bd1482ba1073441e74a1bb1308dc445c88749e0dc4f1889a88a4', + sha1: '23291c758d925b9f4bb3584de3763317e94c6ce9', + }, + }, + established: true, + rtt: { + handshake: { + us: 33103, + }, + }, + version_protocol: 'tls', + }; +}; diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts new file mode 100644 index 0000000000000..05967e0f3acaf --- /dev/null +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -0,0 +1,62 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; +import { makeCheck } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; +import { getSha256 } from '../../../api_integration/apis/uptime/rest/helper/make_tls'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const { uptime } = getPageObjects(['uptime']); + const uptimeService = getService('uptime'); + + const es = getService('es'); + + describe('certificate page', function() { + before(async () => { + await uptime.goToRoot(true); + }); + + beforeEach(async () => { + await makeCheck({ es, tls: true }); + await uptimeService.navigation.refreshApp(); + }); + + it('can navigate to cert page', async () => { + await uptimeService.navigation.refreshApp(); + await uptimeService.cert.hasViewCertButton(); + await uptimeService.navigation.goToCertificates(); + }); + + it('displays certificates', async () => { + await uptimeService.cert.hasCertificates(); + }); + + it('displays specific certificates', async () => { + const certId = getSha256(); + const { monitorId } = await makeCheck({ + es, + tls: { + sha256: certId, + }, + }); + + await uptimeService.navigation.refreshApp(); + await uptimeService.cert.certificateExists({ certId, monitorId }); + }); + + it('performs search against monitor id', async () => { + const certId = getSha256(); + const { monitorId } = await makeCheck({ + es, + tls: { + sha256: certId, + }, + }); + await uptimeService.navigation.refreshApp(); + await uptimeService.cert.searchIsWorking(monitorId); + }); + }); +}; diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index f47214dc2ad2f..6ecd39f696312 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -53,6 +53,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./locations')); loadTestFile(require.resolve('./settings')); + loadTestFile(require.resolve('./certificates')); }); describe('with real-world data', () => { before(async () => { diff --git a/x-pack/test/functional/es_archives/uptime/blank/mappings.json b/x-pack/test/functional/es_archives/uptime/blank/mappings.json index fff4ef47bce0c..dd7f5cb9aa778 100644 --- a/x-pack/test/functional/es_archives/uptime/blank/mappings.json +++ b/x-pack/test/functional/es_archives/uptime/blank/mappings.json @@ -1665,6 +1665,13 @@ }, "id": { "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, "ignore_above": 1024 }, "ip": { @@ -1672,6 +1679,13 @@ }, "name": { "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, "ignore_above": 1024 }, "status": { @@ -3079,10 +3093,21 @@ }, "x509": { "properties": { + "alternative_names": { + "type": "keyword", + "ignore_above": 1024 + }, "issuer": { "properties": { "common_name": { "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, "ignore_above": 1024 }, "distinguished_name": { @@ -3092,14 +3117,16 @@ } }, "not_after": { - "type": "keyword", - "ignore_above": 1024 + "type": "date" }, "not_before": { + "type": "date" + }, + "public_key_algorithm": { "type": "keyword", "ignore_above": 1024 }, - "public_key_algorithm": { + "public_key_curve": { "type": "keyword", "ignore_above": 1024 }, @@ -3121,6 +3148,13 @@ "properties": { "common_name": { "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, "ignore_above": 1024 }, "distinguished_name": { @@ -3128,6 +3162,10 @@ "ignore_above": 1024 } } + }, + "version_number": { + "type": "keyword", + "ignore_above": 1024 } } } diff --git a/x-pack/test/functional/es_archives/uptime/full_heartbeat/mappings.json b/x-pack/test/functional/es_archives/uptime/full_heartbeat/mappings.json index 2b6002ddb3fab..97b72510da286 100644 --- a/x-pack/test/functional/es_archives/uptime/full_heartbeat/mappings.json +++ b/x-pack/test/functional/es_archives/uptime/full_heartbeat/mappings.json @@ -12,79 +12,115 @@ "beat": "heartbeat", "version": "8.0.0" }, - "date_detection": false, "dynamic_templates": [ { "labels": { + "path_match": "labels.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" + } } }, { "container.labels": { + "path_match": "container.labels.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "container.labels.*" + } } }, { "dns.answers": { + "path_match": "dns.answers.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, + } + } + }, + { + "log.syslog": { + "path_match": "log.syslog.*", "match_mapping_type": "string", - "path_match": "dns.answers.*" + "mapping": { + "type": "keyword" + } } }, { - "fields": { + "network.inner": { + "path_match": "network.inner.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, + } + } + }, + { + "observer.egress": { + "path_match": "observer.egress.*", "match_mapping_type": "string", - "path_match": "fields.*" + "mapping": { + "type": "keyword" + } } }, { - "docker.container.labels": { + "observer.ingress": { + "path_match": "observer.ingress.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, + } + } + }, + { + "fields": { + "path_match": "fields.*", + "match_mapping_type": "string", + "mapping": { + "type": "keyword" + } + } + }, + { + "docker.container.labels": { + "path_match": "docker.container.labels.*", "match_mapping_type": "string", - "path_match": "docker.container.labels.*" + "mapping": { + "type": "keyword" + } } }, { "kubernetes.labels.*": { + "path_match": "kubernetes.labels.*", "mapping": { "type": "keyword" - }, - "path_match": "kubernetes.labels.*" + } } }, { "kubernetes.annotations.*": { + "path_match": "kubernetes.annotations.*", "mapping": { "type": "keyword" - }, - "path_match": "kubernetes.annotations.*" + } } }, { "strings_as_keyword": { + "match_mapping_type": "string", "mapping": { "ignore_above": 1024, "type": "keyword" - }, - "match_mapping_type": "string" + } } } ], + "date_detection": false, "properties": { "@timestamp": { "type": "date" @@ -92,28 +128,28 @@ "agent": { "properties": { "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "hostname": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -125,8 +161,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -135,8 +177,8 @@ "client": { "properties": { "address": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "as": { "properties": { @@ -146,8 +188,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -157,41 +205,41 @@ "type": "long" }, "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -199,8 +247,8 @@ "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "nat": { "properties": { @@ -218,43 +266,67 @@ "port": { "type": "long" }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 + }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -265,76 +337,97 @@ "account": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "availability_zone": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "image": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "instance": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "machine": { "properties": { "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "project": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "provider": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" } } }, "container": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "image": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "tag": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -342,20 +435,20 @@ "type": "object" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "runtime": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "destination": { "properties": { "address": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "as": { "properties": { @@ -365,8 +458,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -376,41 +475,41 @@ "type": "long" }, "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -418,8 +517,8 @@ "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "nat": { "properties": { @@ -437,43 +536,144 @@ "port": { "type": "long" }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 + }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha512": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "pe": { + "properties": { + "company": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "file_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "original_file_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "product": { + "type": "keyword", + "ignore_above": 1024 } } } @@ -484,55 +684,63 @@ "answers": { "properties": { "class": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "data": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "ttl": { "type": "long" }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "header_flags": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "op_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "question": { "properties": { "class": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "registered_domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "subdomain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -540,12 +748,12 @@ "type": "ip" }, "response_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -563,51 +771,61 @@ "ecs": { "properties": { "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "error": { "properties": { "code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "message": { - "norms": false, - "type": "text" + "type": "text", + "norms": false + }, + "stack_trace": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "event": { "properties": { "action": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "category": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "created": { "type": "date" }, "dataset": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "duration": { "type": "long" @@ -616,32 +834,39 @@ "type": "date" }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "ingested": { + "type": "date" }, "kind": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "module": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "original": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "outcome": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "provider": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 }, "risk_score": { "type": "float" @@ -659,12 +884,16 @@ "type": "date" }, "timezone": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "url": { + "type": "keyword", + "ignore_above": 1024 } } }, @@ -676,6 +905,31 @@ "accessed": { "type": "date" }, + "attributes": { + "type": "keyword", + "ignore_above": 1024 + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, "created": { "type": "date" }, @@ -683,254 +937,318 @@ "type": "date" }, "device": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "directory": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "drive_letter": { + "type": "keyword", + "ignore_above": 1 }, "extension": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "gid": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "group": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "hash": { "properties": { "md5": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha1": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha256": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha512": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "inode": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "mime_type": { + "type": "keyword", + "ignore_above": 1024 }, "mode": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "mtime": { "type": "date" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "owner": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "path": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "pe": { + "properties": { + "company": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "file_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "original_file_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "product": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "size": { "type": "long" }, "target_path": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "uid": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { "properties": { "md5": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha1": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha256": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha512": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "host": { "properties": { "architecture": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "containerized": { "type": "boolean" }, + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hostname": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "ip": { "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "os": { "properties": { "build": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "codename": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "family": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "kernel": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "platform": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "uptime": { "type": "long" @@ -938,40 +1256,56 @@ "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -987,8 +1321,14 @@ "type": "long" }, "content": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } }, @@ -996,12 +1336,12 @@ "type": "long" }, "method": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "referrer": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1013,18 +1353,28 @@ "type": "long" }, "content": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "bytes": { "type": "long" }, + "redirects": { + "type": "keyword", + "ignore_above": 1024 + }, "status_code": { "type": "long" } @@ -1077,8 +1427,8 @@ } }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1096,17 +1446,33 @@ } } }, + "interface": { + "properties": { + "alias": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, "jolokia": { "properties": { "agent": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1116,22 +1482,22 @@ "server": { "properties": { "product": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "vendor": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "url": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1147,20 +1513,20 @@ "container": { "properties": { "image": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "deployment": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1172,42 +1538,42 @@ } }, "namespace": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "node": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "pod": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "uid": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "replicaset": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "statefulset": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } } @@ -1219,28 +1585,76 @@ "log": { "properties": { "level": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "logger": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "function": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "original": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } } } }, "message": { - "norms": false, - "type": "text" + "type": "text", + "norms": false }, "monitor": { "properties": { "check_group": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "duration": { "properties": { @@ -1250,238 +1664,683 @@ } }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, + "ignore_above": 1024 }, "ip": { "type": "ip" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, + "ignore_above": 1024 }, "status": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "timespan": { + "type": "date_range" }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "network": { "properties": { "application": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "bytes": { "type": "long" }, "community_id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "direction": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "forwarded_ip": { "type": "ip" }, "iana_number": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "packets": { "type": "long" }, "protocol": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "transport": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } } } }, "observer": { "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "zone": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hostname": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "zone": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "ip": { "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 }, "os": { "properties": { "family": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "kernel": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "platform": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, + "product": { + "type": "keyword", + "ignore_above": 1024 + }, "serial_number": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "vendor": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "organization": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } }, "os": { "properties": { "family": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "kernel": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "platform": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "package": { + "properties": { + "architecture": { + "type": "keyword", + "ignore_above": 1024 + }, + "build_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "checksum": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "install_scope": { + "type": "keyword", + "ignore_above": 1024 + }, + "installed": { + "type": "date" + }, + "license": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + }, + "size": { + "type": "long" + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "pe": { + "properties": { + "company": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "file_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "original_file_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "product": { + "type": "keyword", + "ignore_above": 1024 } } }, "process": { "properties": { "args": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, - "executable": { - "ignore_above": 1024, - "type": "keyword" + "args_count": { + "type": "long" }, - "hash": { + "code_signature": { "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" + "exists": { + "type": "boolean" }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" + "status": { + "type": "keyword", + "ignore_above": 1024 }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" + "subject_name": { + "type": "keyword", + "ignore_above": 1024 }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "entity_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "executable": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha512": { + "type": "keyword", + "ignore_above": 1024 } } }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "parent": { + "properties": { + "args": { + "type": "keyword", + "ignore_above": 1024 + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "entity_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "executable": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha512": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "name": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "title": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + } + } + }, + "pe": { + "properties": { + "company": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "file_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "original_file_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "product": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "pgid": { "type": "long" @@ -1501,28 +2360,84 @@ "type": "long" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "title": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "uptime": { "type": "long" }, "working_directory": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "type": "keyword", + "ignore_above": 1024 + }, + "strings": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hive": { + "type": "keyword", + "ignore_above": 1024 + }, + "key": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "value": { + "type": "keyword", + "ignore_above": 1024 } } }, "related": { "properties": { + "hash": { + "type": "keyword", + "ignore_above": 1024 + }, "ip": { "type": "ip" + }, + "user": { + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1540,11 +2455,55 @@ } } }, + "rule": { + "properties": { + "author": { + "type": "keyword", + "ignore_above": 1024 + }, + "category": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "license": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + }, + "ruleset": { + "type": "keyword", + "ignore_above": 1024 + }, + "uuid": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, "server": { "properties": { "address": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "as": { "properties": { @@ -1554,8 +2513,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -1565,41 +2530,41 @@ "type": "long" }, "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1607,8 +2572,8 @@ "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "nat": { "properties": { @@ -1626,43 +2591,67 @@ "port": { "type": "long" }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 + }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -1671,28 +2660,36 @@ "service": { "properties": { "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "node": { + "properties": { + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "state": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1714,8 +2711,8 @@ "source": { "properties": { "address": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "as": { "properties": { @@ -1725,8 +2722,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -1736,41 +2739,41 @@ "type": "long" }, "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1778,8 +2781,8 @@ "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "nat": { "properties": { @@ -1797,43 +2800,67 @@ "port": { "type": "long" }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 + }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -1850,8 +2877,8 @@ } }, "tags": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "tcp": { "properties": { @@ -1875,11 +2902,57 @@ } } }, + "threat": { + "properties": { + "framework": { + "type": "keyword", + "ignore_above": 1024 + }, + "tactic": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "technique": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + }, "timeseries": { "properties": { "instance": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1891,6 +2964,78 @@ "certificate_not_valid_before": { "type": "date" }, + "cipher": { + "type": "keyword", + "ignore_above": 1024 + }, + "client": { + "properties": { + "certificate": { + "type": "keyword", + "ignore_above": 1024 + }, + "certificate_chain": { + "type": "keyword", + "ignore_above": 1024 + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "issuer": { + "type": "keyword", + "ignore_above": 1024 + }, + "ja3": { + "type": "keyword", + "ignore_above": 1024 + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject": { + "type": "keyword", + "ignore_above": 1024 + }, + "supported_ciphers": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "curve": { + "type": "keyword", + "ignore_above": 1024 + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "type": "keyword", + "ignore_above": 1024 + }, + "resumed": { + "type": "boolean" + }, "rtt": { "properties": { "handshake": { @@ -1901,6 +3046,138 @@ } } } + }, + "server": { + "properties": { + "certificate": { + "type": "keyword", + "ignore_above": 1024 + }, + "certificate_chain": { + "type": "keyword", + "ignore_above": 1024 + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "issuer": { + "type": "keyword", + "ignore_above": 1024 + }, + "ja3s": { + "type": "keyword", + "ignore_above": 1024 + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "type": "keyword", + "ignore_above": 1024 + }, + "x509": { + "properties": { + "alternative_names": { + "type": "keyword", + "ignore_above": 1024 + }, + "issuer": { + "properties": { + "common_name": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, + "ignore_above": 1024 + }, + "distinguished_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "type": "keyword", + "ignore_above": 1024 + }, + "public_key_curve": { + "type": "keyword", + "ignore_above": 1024 + }, + "public_key_exponent": { + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "type": "keyword", + "ignore_above": 1024 + }, + "signature_algorithm": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject": { + "properties": { + "common_name": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, + "ignore_above": 1024 + }, + "distinguished_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "version_number": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + }, + "version_protocol": { + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1909,16 +3186,16 @@ "trace": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "transaction": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } } @@ -1927,83 +3204,123 @@ "url": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "extension": { + "type": "keyword", + "ignore_above": 1024 }, "fragment": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "original": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "password": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "path": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "port": { "type": "long" }, "query": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 }, "scheme": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 }, "username": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } }, @@ -2012,50 +3329,147 @@ "device": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "original": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "os": { "properties": { "family": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "kernel": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "platform": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "vulnerability": { + "properties": { + "category": { + "type": "keyword", + "ignore_above": 1024 + }, + "classification": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "enumeration": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + }, + "report_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "scanner": { + "properties": { + "vendor": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "severity": { + "type": "keyword", + "ignore_above": 1024 } } } diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index 0ebcb5c87deee..53c89eadeced7 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -13,8 +13,11 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo const retry = getService('retry'); return new (class UptimePage { - public async goToRoot() { + public async goToRoot(refresh?: boolean) { await navigation.goToUptime(); + if (refresh) { + await navigation.refreshApp(); + } } public async setDateRange(start: string, end: string) { diff --git a/x-pack/test/functional/services/uptime/certificates.ts b/x-pack/test/functional/services/uptime/certificates.ts new file mode 100644 index 0000000000000..fb7cb6191b0ae --- /dev/null +++ b/x-pack/test/functional/services/uptime/certificates.ts @@ -0,0 +1,50 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function UptimeCertProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + const changeSearchField = async (text: string) => { + const input = await testSubjects.find('uptimeCertSearch'); + await input.clearValueWithKeyboard(); + await input.type(text); + }; + + return { + async hasViewCertButton() { + return retry.tryForTime(15000, async () => { + await testSubjects.existOrFail('uptimeCertificatesLink'); + }); + }, + async certificateExists(cert: { certId: string; monitorId: string }) { + return retry.tryForTime(15000, async () => { + await testSubjects.existOrFail(cert.certId); + await testSubjects.existOrFail('monitor-page-link-' + cert.monitorId); + }); + }, + async hasCertificates(expectedTotal?: number) { + return retry.tryForTime(15000, async () => { + const totalCerts = await testSubjects.getVisibleText('uptimeCertTotal'); + if (expectedTotal) { + expect(Number(totalCerts) === expectedTotal).to.eql(true); + } else { + expect(Number(totalCerts) > 0).to.eql(true); + } + }); + }, + async searchIsWorking(monId: string) { + const self = this; + return retry.tryForTime(15000, async () => { + await changeSearchField(monId); + await self.hasCertificates(1); + }); + }, + }; +} diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index 36a5d7c9702f8..c17fa3a5f6339 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -25,9 +25,13 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv }); }; + const refreshApp = async () => { + await testSubjects.click('superDatePickerApplyTimeButton'); + }; + return { async refreshApp() { - await testSubjects.click('superDatePickerApplyTimeButton'); + await refreshApp(); }, async goToUptime() { @@ -60,6 +64,13 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv } }, + goToCertificates: async () => { + return retry.tryForTime(30 * 1000, async () => { + await testSubjects.click('uptimeCertificatesLink'); + await testSubjects.existOrFail('uptimeCertificatesPage'); + }); + }, + async loadDataAndGoToMonitorPage(dateStart: string, dateEnd: string, monitorId: string) { await PageObjects.timePicker.setAbsoluteRange(dateStart, dateEnd); await this.goToMonitor(monitorId); diff --git a/x-pack/test/functional/services/uptime/uptime.ts b/x-pack/test/functional/services/uptime/uptime.ts index 601feb6b0646e..8d36ba4bf6cfd 100644 --- a/x-pack/test/functional/services/uptime/uptime.ts +++ b/x-pack/test/functional/services/uptime/uptime.ts @@ -12,6 +12,7 @@ import { UptimeMonitorProvider } from './monitor'; import { UptimeNavigationProvider } from './navigation'; import { UptimeAlertsProvider } from './alerts'; import { UptimeMLAnomalyProvider } from './ml_anomaly'; +import { UptimeCertProvider } from './certificates'; export function UptimeProvider(context: FtrProviderContext) { const common = UptimeCommonProvider(context); @@ -20,6 +21,7 @@ export function UptimeProvider(context: FtrProviderContext) { const navigation = UptimeNavigationProvider(context); const alerts = UptimeAlertsProvider(context); const ml = UptimeMLAnomalyProvider(context); + const cert = UptimeCertProvider(context); return { common, @@ -28,5 +30,6 @@ export function UptimeProvider(context: FtrProviderContext) { navigation, alerts, ml, + cert, }; } From 856a82046ddd612c48551c9bcde2d3de0a3f3b5b Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Fri, 1 May 2020 10:42:05 -0400 Subject: [PATCH 066/122] [EPM] fix updates available filter (#64957) * fix filter * remove unneeded installationVersion from package * use filter instead of reduce * fix type error --- .../ingest_manager/common/types/models/epm.ts | 1 - .../sections/epm/screens/detail/header.tsx | 7 ++++++- .../sections/epm/screens/detail/index.tsx | 5 ++++- .../sections/epm/screens/home/index.tsx | 20 +++++++++++-------- .../ingest_manager/types/index.ts | 1 + .../server/services/epm/packages/index.ts | 1 - 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index f8779a879a049..82de90e4735f2 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -205,7 +205,6 @@ export interface RegistryVarsEntry { interface PackageAdditions { title: string; latestVersion: string; - installedVersion?: string; assets: AssetsGroupedByServiceByType; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx index d20350c5db631..cf51296d468a9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx @@ -29,7 +29,12 @@ const Text = styled.span` type HeaderProps = PackageInfo & { iconType?: IconType }; export function Header(props: HeaderProps) { - const { iconType, name, title, version, installedVersion, latestVersion } = props; + const { iconType, name, title, version, latestVersion } = props; + + let installedVersion; + if ('savedObject' in props) { + installedVersion = props.savedObject.attributes.version; + } const hasWriteCapabilites = useCapabilities().write; const { toListView } = useLinks(); const ADD_DATASOURCE_URI = useLink(`${EPM_PATH}/${name}-${version}/add-datasource`); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx index 1f3eb2cc9362e..848d278819d1d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx @@ -32,7 +32,10 @@ export function Detail() { const packageInfo = response.data?.response; const title = packageInfo?.title; const name = packageInfo?.name; - const installedVersion = packageInfo?.installedVersion; + let installedVersion; + if (packageInfo && 'savedObject' in packageInfo) { + installedVersion = packageInfo.savedObject.attributes.version; + } const status: InstallStatus = packageInfo?.status as any; // track install status state diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx index bf785147502b5..983a322de1088 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx @@ -67,29 +67,34 @@ export function EPMHomePage() { function InstalledPackages() { const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages(); const [selectedCategory, setSelectedCategory] = useState(''); - const packages = - allPackages && allPackages.response && selectedCategory === '' - ? allPackages.response.filter(pkg => pkg.status === 'installed') - : []; const title = i18n.translate('xpack.ingestManager.epmList.installedTitle', { defaultMessage: 'Installed integrations', }); + const allInstalledPackages = + allPackages && allPackages.response + ? allPackages.response.filter(pkg => pkg.status === 'installed') + : []; + + const updatablePackages = allInstalledPackages.filter( + item => 'savedObject' in item && item.version > item.savedObject.attributes.version + ); + const categories = [ { id: '', title: i18n.translate('xpack.ingestManager.epmList.allFilterLinkText', { defaultMessage: 'All', }), - count: packages.length, + count: allInstalledPackages.length, }, { id: 'updates_available', title: i18n.translate('xpack.ingestManager.epmList.updatesAvailableFilterLinkText', { defaultMessage: 'Updates available', }), - count: 0, // TODO: Update with real count when available + count: updatablePackages.length, }, ]; @@ -106,7 +111,7 @@ function InstalledPackages() { isLoading={isLoadingPackages} controls={controls} title={title} - list={packages} + list={selectedCategory === 'updates_available' ? updatablePackages : allInstalledPackages} /> ); } @@ -134,7 +139,6 @@ function AvailablePackages() { }, ...(categoriesRes ? categoriesRes.response : []), ]; - const controls = categories ? ( ( ? { ...from, status: InstallationStatus.installed, - installedVersion: savedObject.attributes.version, savedObject, } : { From d314e4624e49e264ea32fd1585b4edb1ca5df779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Sat, 2 May 2020 22:12:13 +0200 Subject: [PATCH 067/122] Use HDR for percentiles (#64758) --- .../__snapshots__/fetcher.test.ts.snap | 6 ++++++ .../__snapshots__/queries.test.ts.snap | 6 ++++++ .../plugins/apm/server/lib/transaction_groups/fetcher.ts | 6 +++++- .../lib/transactions/__snapshots__/queries.test.ts.snap | 9 +++++++++ .../__snapshots__/fetcher.test.ts.snap | 3 +++ .../transactions/charts/get_timeseries_data/fetcher.ts | 6 +++++- x-pack/plugins/apm/typings/elasticsearch/aggregations.ts | 1 + 7 files changed, 35 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap index 580cafff95e0c..64f06ad0a81cd 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap @@ -16,6 +16,9 @@ Array [ "p95": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, ], @@ -126,6 +129,9 @@ Array [ "p95": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, ], diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index 1096c1638f3f2..b93f842b878cb 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -14,6 +14,9 @@ Object { "p95": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, ], @@ -120,6 +123,9 @@ Object { "p95": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, ], diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 39f2be551ab6e..fb1aafc2d6c95 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -83,7 +83,11 @@ export function transactionGroupsFetcher( sample: { top_hits: { size: 1, sort } }, avg: { avg: { field: TRANSACTION_DURATION } }, p95: { - percentiles: { field: TRANSACTION_DURATION, percents: [95] } + percentiles: { + field: TRANSACTION_DURATION, + percents: [95], + hdr: { number_of_significant_value_digits: 2 } + } }, sum: { sum: { field: TRANSACTION_DURATION } } } diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index 49e0e0669c241..cc5900919f829 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -333,6 +333,9 @@ Object { "pct": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, 99, @@ -425,6 +428,9 @@ Object { "pct": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, 99, @@ -522,6 +528,9 @@ Object { "pct": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, 99, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap index 6c8430a3e71cf..25ebb15fd73e8 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap @@ -21,6 +21,9 @@ Array [ "pct": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, 99, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 8a2e01c9a7891..e33b98592da2d 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -69,7 +69,11 @@ export function timeseriesFetcher({ aggs: { avg: { avg: { field: TRANSACTION_DURATION } }, pct: { - percentiles: { field: TRANSACTION_DURATION, percents: [95, 99] } + percentiles: { + field: TRANSACTION_DURATION, + percents: [95, 99], + hdr: { number_of_significant_value_digits: 2 } + } } } }, diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index 8a8d256cf4273..0739e8e6120bf 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -86,6 +86,7 @@ export interface AggregationOptionsByType { percentiles: { field: string; percents?: number[]; + hdr?: { number_of_significant_value_digits: number }; }; extended_stats: { field: string; From 8eeaf96cf57f910c0944ba0d5a0609bfd0c11e1c Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Sat, 2 May 2020 23:36:26 +0200 Subject: [PATCH 068/122] [APM] Fix paths for ts optimization script (#65012) --- x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js index cab55a2526202..aeccd403c5ce6 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js @@ -5,7 +5,7 @@ */ const path = require('path'); -const xpackRoot = path.resolve(__dirname, '../../../../..'); +const xpackRoot = path.resolve(__dirname, '../../../..'); const kibanaRoot = path.resolve(xpackRoot, '..'); const tsconfigTpl = path.resolve(__dirname, './tsconfig.json'); From c995a333de4384f14233dcd61031267fbb6b50bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Sun, 3 May 2020 09:19:16 +0200 Subject: [PATCH 069/122] [APM] Fix failing `ApmIndices` test (#64965) * [APM] Fix failing Indicies Settings test * Cleanup Co-authored-by: Elastic Machine --- .../components/app/Settings/ApmIndices/index.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx index 272c4b3add415..b03960861e0ad 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { render, wait } from '@testing-library/react'; +import { render } from '@testing-library/react'; import React from 'react'; import { ApmIndices } from '.'; import * as hooks from '../../../../hooks/useFetcher'; import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; describe('ApmIndices', () => { - it('should not get stuck in infinite loop', async () => { - spyOn(hooks, 'useFetcher').and.returnValue({ + it('should not get stuck in infinite loop', () => { + const spy = spyOn(hooks, 'useFetcher').and.returnValue({ data: undefined, status: 'loading' }); @@ -30,6 +30,6 @@ describe('ApmIndices', () => { `); - await wait(); + expect(spy).toHaveBeenCalledTimes(2); }); }); From 4d19323150e9f09fab8fdd5e85cd81a2e494bbf6 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 4 May 2020 10:29:29 +0200 Subject: [PATCH 070/122] onEvent prop for expression component (#64995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add onEvent prop to expression component * feat: 🎸 add type safety to onEvent prop in expression component Co-authored-by: Elastic Machine --- src/plugins/expressions/public/index.ts | 2 +- .../public/react_expression_renderer.test.tsx | 41 +++++++++++++++++++ .../public/react_expression_renderer.tsx | 12 +++++- src/plugins/expressions/public/render.ts | 6 +-- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index c57db6029ec2e..6814764ee5faa 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -37,7 +37,7 @@ export { ReactExpressionRendererProps, ReactExpressionRendererType, } from './react_expression_renderer'; -export { ExpressionRenderHandler } from './render'; +export { ExpressionRenderHandler, ExpressionRendererEvent } from './render'; export { AnyExpressionFunctionDefinition, AnyExpressionTypeDefinition, diff --git a/src/plugins/expressions/public/react_expression_renderer.test.tsx b/src/plugins/expressions/public/react_expression_renderer.test.tsx index 65cc5fc1569cb..caa9bc68dffb8 100644 --- a/src/plugins/expressions/public/react_expression_renderer.test.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.test.tsx @@ -26,6 +26,7 @@ import { ExpressionLoader } from './loader'; import { mount } from 'enzyme'; import { EuiProgress } from '@elastic/eui'; import { RenderErrorHandlerFnType } from './types'; +import { ExpressionRendererEvent } from './render'; jest.mock('./loader', () => { return { @@ -135,4 +136,44 @@ describe('ExpressionRenderer', () => { expect(instance.find(EuiProgress)).toHaveLength(0); expect(instance.find('[data-test-subj="custom-error"]')).toHaveLength(0); }); + + it('should fire onEvent prop on every events$ observable emission in loader', () => { + const dataSubject = new Subject(); + const data$ = dataSubject.asObservable().pipe(share()); + const renderSubject = new Subject(); + const render$ = renderSubject.asObservable().pipe(share()); + const loadingSubject = new Subject(); + const loading$ = loadingSubject.asObservable().pipe(share()); + const eventsSubject = new Subject(); + const events$ = eventsSubject.asObservable().pipe(share()); + + const onEvent = jest.fn(); + const event: ExpressionRendererEvent = { + name: 'foo', + data: { + bar: 'baz', + }, + }; + + (ExpressionLoader as jest.Mock).mockImplementation(() => { + return { + render$, + data$, + loading$, + events$, + update: jest.fn(), + }; + }); + + mount(); + + expect(onEvent).toHaveBeenCalledTimes(0); + + act(() => { + eventsSubject.next(event); + }); + + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent.mock.calls[0][0]).toBe(event); + }); }); diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index 2c99f173c9f33..9e237d36ef627 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -27,6 +27,7 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { IExpressionLoaderParams, RenderError } from './types'; import { ExpressionAstExpression, IInterpreterRenderHandlers } from '../common'; import { ExpressionLoader } from './loader'; +import { ExpressionRendererEvent } from './render'; // Accept all options of the runner as props except for the // dom element which is provided by the component itself @@ -36,6 +37,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams { expression: string | ExpressionAstExpression; renderError?: (error?: string | null) => React.ReactElement | React.ReactElement[]; padding?: 'xs' | 's' | 'm' | 'l' | 'xl'; + onEvent?: (event: ExpressionRendererEvent) => void; } export type ReactExpressionRendererType = React.ComponentType; @@ -60,6 +62,7 @@ export const ReactExpressionRenderer = ({ padding, renderError, expression, + onEvent, ...expressionLoaderOptions }: ReactExpressionRendererProps) => { const mountpoint: React.MutableRefObject = useRef(null); @@ -99,6 +102,13 @@ export const ReactExpressionRenderer = ({ } : expressionLoaderOptions.onRenderError, }); + if (onEvent) { + subs.push( + expressionLoaderRef.current.events$.subscribe(event => { + onEvent(event); + }) + ); + } subs.push( expressionLoaderRef.current.loading$.subscribe(() => { hasHandledErrorRef.current = false; @@ -123,7 +133,7 @@ export const ReactExpressionRenderer = ({ errorRenderHandlerRef.current = null; }; - }, [hasCustomRenderErrorHandler]); + }, [hasCustomRenderErrorHandler, onEvent]); // Re-fetch data automatically when the inputs change useShallowCompareEffect( diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index 4aaf0da60fc60..c8a4022a01131 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -32,7 +32,7 @@ export interface ExpressionRenderHandlerParams { onRenderError: RenderErrorHandlerFnType; } -interface Event { +export interface ExpressionRendererEvent { name: string; data: any; } @@ -45,7 +45,7 @@ interface UpdateValue { export class ExpressionRenderHandler { render$: Observable; update$: Observable; - events$: Observable; + events$: Observable; private element: HTMLElement; private destroyFn?: any; @@ -63,7 +63,7 @@ export class ExpressionRenderHandler { this.element = element; this.eventsSubject = new Rx.Subject(); - this.events$ = this.eventsSubject.asObservable() as Observable; + this.events$ = this.eventsSubject.asObservable() as Observable; this.onRenderError = onRenderError || defaultRenderErrorHandler; From 007b16793dc170b5283e99d47d98aef379c41d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 4 May 2020 12:18:19 +0200 Subject: [PATCH 071/122] Bump backport to 5.4.1 (#65041) --- .backportrc.json | 29 +++++++++++++++++++++++++++-- package.json | 2 +- yarn.lock | 47 ++++++++++++++++++++++++++--------------------- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/.backportrc.json b/.backportrc.json index 2603eb2e2d444..731f49183dba5 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -1,5 +1,30 @@ { "upstream": "elastic/kibana", - "branches": [{ "name": "7.x", "checked": true }, "7.7", "7.6", "7.5", "7.4", "7.3", "7.2", "7.1", "7.0", "6.8", "6.7", "6.6", "6.5", "6.4", "6.3", "6.2", "6.1", "6.0", "5.6"], - "labels": ["backport"] + "targetBranchChoices": [ + { "name": "master", "checked": true }, + { "name": "7.x", "checked": true }, + "7.7", + "7.6", + "7.5", + "7.4", + "7.3", + "7.2", + "7.1", + "7.0", + "6.8", + "6.7", + "6.6", + "6.5", + "6.4", + "6.3", + "6.2", + "6.1", + "6.0", + "5.6" + ], + "targetPRLabels": ["backport"], + "branchLabelMapping": { + "^v7.8.0$": "7.x", + "^v(\\d+).(\\d+).\\d+$": "$1.$2" + } } diff --git a/package.json b/package.json index 0ad304fdf2f69..1e3ddc976aa67 100644 --- a/package.json +++ b/package.json @@ -400,7 +400,7 @@ "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", "babel-plugin-istanbul": "^6.0.0", - "backport": "5.1.3", + "backport": "5.4.1", "chai": "3.5.0", "chance": "1.0.18", "cheerio": "0.22.0", diff --git a/yarn.lock b/yarn.lock index 94e6a0a11aa99..346c4d76d24c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4778,11 +4778,6 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/safe-json-stringify@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@types/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz#4cd786442d7abc037f8e9026b22e3b401005c287" - integrity sha512-iIQqHp8fqDgxTlWor4DrTrKGVmjDeGDodQBipQkPSlRU1QeKIytv37U4aFN9N65VJcFJx67+zOnpbTNQzqHTOg== - "@types/seedrandom@>=2.0.0 <4.0.0": version "2.4.28" resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-2.4.28.tgz#9ce8fa048c1e8c85cb71d7fe4d704e000226036f" @@ -7409,24 +7404,24 @@ backo2@1.0.2: resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= -backport@5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/backport/-/backport-5.1.3.tgz#6fa788f48ee90e2b98b4e8d6d9385b0fb2e2f689" - integrity sha512-fTrXAyXvsg+lOuuWQosHzz/YnFfrkBsVkPcygjrDZVlWhbD+cA8mY3GrcJ8sIFwUg9Ja8qCeBFfLIRKlOwuzEg== +backport@5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/backport/-/backport-5.4.1.tgz#b066e8bbece91bc813187c13b7bea69ef5355471" + integrity sha512-vFR5Juss2pveS2OyyoE5n14j7ZDqeZXakzv4KngTEUTsb+5r/AVj2OG8LfJ14RJBMKBYSf1ojSKgDiWtUi0r+w== dependencies: - "@types/safe-json-stringify" "^1.1.0" axios "^0.19.2" dedent "^0.7.0" del "^5.1.0" find-up "^4.1.0" inquirer "^7.1.0" + lodash.flatmap "^4.5.0" lodash.isempty "^4.4.0" lodash.isstring "^4.0.1" lodash.uniq "^4.5.0" - make-dir "^3.0.2" - ora "^4.0.3" + make-dir "^3.1.0" + ora "^4.0.4" safe-json-stringify "^1.2.0" - strip-json-comments "^3.0.1" + strip-json-comments "^3.1.0" winston "^3.2.1" yargs "^15.3.1" @@ -19538,6 +19533,11 @@ lodash.filter@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" integrity sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4= +lodash.flatmap@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.flatmap/-/lodash.flatmap-4.5.0.tgz#ef8cbf408f6e48268663345305c6acc0b778702e" + integrity sha1-74y/QI9uSCaGYzRTBcaswLd4cC4= + lodash.flatten@^4.2.0, lodash.flatten@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" @@ -20026,10 +20026,10 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" -make-dir@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392" - integrity sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w== +make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: semver "^6.0.0" @@ -22178,10 +22178,10 @@ ora@^3.0.0: strip-ansi "^5.2.0" wcwidth "^1.0.1" -ora@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.3.tgz#752a1b7b4be4825546a7a3d59256fa523b6b6d05" - integrity sha512-fnDebVFyz309A73cqCipVL1fBZewq4vwgSHfxh43vVy31mbyoQ8sCH3Oeaog/owYOs/lLlGVPCISQonTneg6Pg== +ora@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.4.tgz#e8da697cc5b6a47266655bf68e0fb588d29a545d" + integrity sha512-77iGeVU1cIdRhgFzCK8aw1fbtT1B/iZAvWjS+l/o1x0RShMgxHUZaD2yDpWsNCPwXg9z1ZA78Kbdvr8kBmG/Ww== dependencies: chalk "^3.0.0" cli-cursor "^3.1.0" @@ -28174,6 +28174,11 @@ strip-json-comments@^3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== +strip-json-comments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" + integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== + strip-json-comments@~1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" From ccede29e60a6406d5527dbfa21d87497c00b8ddf Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Mon, 4 May 2020 12:12:40 +0100 Subject: [PATCH 072/122] [TSVB] Fixing memory leak (#64918) --- .../server/lib/vis_data/helpers/index.js | 2 ++ .../server/lib/vis_data/helpers/overwrite.js | 31 +++++++++++++++++++ .../annotations/date_histogram.js | 4 +-- .../annotations/top_hits.js | 4 +-- .../series/date_histogram.js | 8 ++--- .../series/filter_ratios.js | 16 ++++++---- .../series/metric_buckets.js | 5 ++- .../series/normalize_query.js | 13 ++++---- .../series/positive_rate.js | 12 ++++--- .../series/sibling_buckets.js | 4 +-- .../series/split_by_everything.js | 4 +-- .../series/split_by_filter.js | 4 +-- .../series/split_by_filters.js | 4 +-- .../series/split_by_terms.js | 18 +++++------ .../table/date_histogram.js | 10 +++--- .../request_processors/table/filter_ratios.js | 12 +++---- .../table/metric_buckets.js | 4 +-- .../table/normalize_query.js | 24 +++++++------- .../request_processors/table/pivot.js | 17 +++++----- .../table/sibling_buckets.js | 4 +-- .../table/split_by_everything.js | 6 ++-- .../table/split_by_terms.js | 8 ++--- 22 files changed, 128 insertions(+), 86 deletions(-) create mode 100644 src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/overwrite.js diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/index.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/index.js index db6365f88d0ff..906730b394ae2 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/index.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/index.js @@ -29,6 +29,8 @@ import { getTimerange } from './get_timerange'; import { mapBucket } from './map_bucket'; import { parseSettings } from './parse_settings'; +export { overwrite } from './overwrite'; + export const helpers = { bucketTransform, getAggValue, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/overwrite.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/overwrite.js new file mode 100644 index 0000000000000..2eba5155a208d --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/overwrite.js @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import set from 'set-value'; + +/** + * Set path in obj. Behaves like lodash `set` + * @param obj The object to mutate + * @param path The path of the sub-property to set + * @param val The value to set the sub-property to + */ +export function overwrite(obj, path, val) { + set(obj, path, undefined); + set(obj, path, val); +} diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js index 283f2c115d4f5..f7b5cc9131ac4 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/date_histogram.js @@ -17,7 +17,7 @@ * under the License. */ -import _ from 'lodash'; +import { overwrite } from '../../helpers'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { getTimerange } from '../../helpers/get_timerange'; import { search } from '../../../../../../../plugins/data/server'; @@ -37,7 +37,7 @@ export function dateHistogram( const { from, to } = getTimerange(req); const timezone = capabilities.searchTimezone; - _.set(doc, `aggs.${annotation.id}.date_histogram`, { + overwrite(doc, `aggs.${annotation.id}.date_histogram`, { field: timeField, min_doc_count: 0, time_zone: timezone, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js index ae1e0bdc3884c..4cc3fd094cc13 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/annotations/top_hits.js @@ -17,13 +17,13 @@ * under the License. */ -import _ from 'lodash'; +import { overwrite } from '../../helpers'; export function topHits(req, panel, annotation) { return next => doc => { const fields = (annotation.fields && annotation.fields.split(/[,\s]+/)) || []; const timeField = annotation.time_field; - _.set(doc, `aggs.${annotation.id}.aggs.hits.top_hits`, { + overwrite(doc, `aggs.${annotation.id}.aggs.hits.top_hits`, { sort: [ { [timeField]: { order: 'desc' }, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js index df63a14ea5ee4..cc6466145dcdf 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/date_histogram.js @@ -17,7 +17,7 @@ * under the License. */ -import { set } from 'lodash'; +import { overwrite } from '../../helpers'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { offsetTime } from '../../offset_time'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; @@ -34,7 +34,7 @@ export function dateHistogram(req, panel, series, esQueryConfig, indexPatternObj const { from, to } = offsetTime(req, series.offset_time); const timezone = capabilities.searchTimezone; - set(doc, `aggs.${series.id}.aggs.timeseries.date_histogram`, { + overwrite(doc, `aggs.${series.id}.aggs.timeseries.date_histogram`, { field: timeField, min_doc_count: 0, time_zone: timezone, @@ -47,7 +47,7 @@ export function dateHistogram(req, panel, series, esQueryConfig, indexPatternObj }; const getDateHistogramForEntireTimerangeMode = () => - set(doc, `aggs.${series.id}.aggs.timeseries.auto_date_histogram`, { + overwrite(doc, `aggs.${series.id}.aggs.timeseries.auto_date_histogram`, { field: timeField, buckets: 1, }); @@ -58,7 +58,7 @@ export function dateHistogram(req, panel, series, esQueryConfig, indexPatternObj // master - set(doc, `aggs.${series.id}.meta`, { + overwrite(doc, `aggs.${series.id}.meta`, { timeField, intervalString, bucketSize, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js index 32a75b1268d06..0ca562c49b4c7 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/filter_ratios.js @@ -19,16 +19,16 @@ const filter = metric => metric.type === 'filter_ratio'; import { bucketTransform } from '../../helpers/bucket_transform'; -import _ from 'lodash'; +import { overwrite } from '../../helpers'; export function ratios(req, panel, series) { return next => doc => { if (series.metrics.some(filter)) { series.metrics.filter(filter).forEach(metric => { - _.set(doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-numerator.filter`, { + overwrite(doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-numerator.filter`, { query_string: { query: metric.numerator || '*', analyze_wildcard: true }, }); - _.set(doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-denominator.filter`, { + overwrite(doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-denominator.filter`, { query_string: { query: metric.denominator || '*', analyze_wildcard: true }, }); @@ -46,8 +46,12 @@ export function ratios(req, panel, series) { metricAgg = {}; } const aggBody = { metric: metricAgg }; - _.set(doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-numerator.aggs`, aggBody); - _.set( + overwrite( + doc, + `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-numerator.aggs`, + aggBody + ); + overwrite( doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}-denominator.aggs`, aggBody @@ -56,7 +60,7 @@ export function ratios(req, panel, series) { denominatorPath = `${metric.id}-denominator>metric`; } - _.set(doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}`, { + overwrite(doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}`, { bucket_script: { buckets_path: { numerator: numeratorPath, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js index 857f2ab1d0485..d390821f9ad98 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/metric_buckets.js @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -import _ from 'lodash'; +import { overwrite } from '../../helpers'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { bucketTransform } from '../../helpers/bucket_transform'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; @@ -33,7 +32,7 @@ export function metricBuckets(req, panel, series, esQueryConfig, indexPatternObj if (fn) { try { const bucket = fn(metric, series.metrics, intervalString); - _.set(doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}`, bucket); + overwrite(doc, `aggs.${series.id}.aggs.timeseries.aggs.${metric.id}`, bucket); } catch (e) { // meh } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.js index 0a701d1de577f..f76f3a531a37d 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/normalize_query.js @@ -16,9 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -const { set, get, isEmpty } = require('lodash'); +import { overwrite } from '../../helpers'; +import _ from 'lodash'; -const isEmptyFilter = (filter = {}) => Boolean(filter.match_all) && isEmpty(filter.match_all); +const isEmptyFilter = (filter = {}) => Boolean(filter.match_all) && _.isEmpty(filter.match_all); const hasSiblingPipelineAggregation = (aggs = {}) => Object.keys(aggs).length > 1; /* For grouping by the 'Everything', the splitByEverything request processor @@ -30,12 +31,12 @@ const hasSiblingPipelineAggregation = (aggs = {}) => Object.keys(aggs).length > * */ function removeEmptyTopLevelAggregation(doc, series) { - const filter = get(doc, `aggs.${series.id}.filter`); + const filter = _.get(doc, `aggs.${series.id}.filter`); if (isEmptyFilter(filter) && !hasSiblingPipelineAggregation(doc.aggs[series.id].aggs)) { - const meta = get(doc, `aggs.${series.id}.meta`); - set(doc, `aggs`, doc.aggs[series.id].aggs); - set(doc, `aggs.timeseries.meta`, meta); + const meta = _.get(doc, `aggs.${series.id}.meta`); + overwrite(doc, `aggs`, doc.aggs[series.id].aggs); + overwrite(doc, `aggs.timeseries.meta`, meta); } return doc; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js index 1ff548cc19e02..45db28fa98f5e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/positive_rate.js @@ -20,7 +20,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { bucketTransform } from '../../helpers/bucket_transform'; -import { set } from 'lodash'; +import { overwrite } from '../../helpers'; export const filter = metric => metric.type === 'positive_rate'; @@ -48,9 +48,13 @@ export const createPositiveRate = (doc, intervalString, aggRoot) => metric => { const derivativeBucket = derivativeFn(derivativeMetric, fakeSeriesMetrics, intervalString); const positiveOnlyBucket = positiveOnlyFn(positiveOnlyMetric, fakeSeriesMetrics, intervalString); - set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-positive-rate-max`, maxBucket); - set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-positive-rate-derivative`, derivativeBucket); - set(doc, `${aggRoot}.timeseries.aggs.${metric.id}`, positiveOnlyBucket); + overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}-positive-rate-max`, maxBucket); + overwrite( + doc, + `${aggRoot}.timeseries.aggs.${metric.id}-positive-rate-derivative`, + derivativeBucket + ); + overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}`, positiveOnlyBucket); }; export function positiveRate(req, panel, series, esQueryConfig, indexPatternObject, capabilities) { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js index bbb7d60c8ef06..d677b2564c940 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/sibling_buckets.js @@ -17,7 +17,7 @@ * under the License. */ -import _ from 'lodash'; +import { overwrite } from '../../helpers'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { bucketTransform } from '../../helpers/bucket_transform'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; @@ -40,7 +40,7 @@ export function siblingBuckets( if (fn) { try { const bucket = fn(metric, series.metrics, bucketSize); - _.set(doc, `aggs.${series.id}.aggs.${metric.id}`, bucket); + overwrite(doc, `aggs.${series.id}.aggs.${metric.id}`, bucket); } catch (e) { // meh } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_everything.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_everything.js index 54424bed0688b..c567e8ded0e61 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_everything.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_everything.js @@ -17,7 +17,7 @@ * under the License. */ -import _ from 'lodash'; +import { overwrite } from '../../helpers'; export function splitByEverything(req, panel, series) { return next => doc => { @@ -25,7 +25,7 @@ export function splitByEverything(req, panel, series) { series.split_mode === 'everything' || (series.split_mode === 'terms' && !series.terms_field) ) { - _.set(doc, `aggs.${series.id}.filter.match_all`, {}); + overwrite(doc, `aggs.${series.id}.filter.match_all`, {}); } return next(doc); }; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js index 80b4ef70a3f08..0822878aa9178 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filter.js @@ -17,7 +17,7 @@ * under the License. */ -import { set } from 'lodash'; +import { overwrite } from '../../helpers'; import { esQuery } from '../../../../../../data/server'; export function splitByFilter(req, panel, series, esQueryConfig, indexPattern) { @@ -26,7 +26,7 @@ export function splitByFilter(req, panel, series, esQueryConfig, indexPattern) { return next(doc); } - set( + overwrite( doc, `aggs.${series.id}.filter`, esQuery.buildEsQuery(indexPattern, [series.filter], [], esQueryConfig) diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js index d023c28cdb25e..a3d2725ef58b5 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_filters.js @@ -17,7 +17,7 @@ * under the License. */ -import { set } from 'lodash'; +import { overwrite } from '../../helpers'; import { esQuery } from '../../../../../../data/server'; export function splitByFilters(req, panel, series, esQueryConfig, indexPattern) { @@ -26,7 +26,7 @@ export function splitByFilters(req, panel, series, esQueryConfig, indexPattern) series.split_filters.forEach(filter => { const builtEsQuery = esQuery.buildEsQuery(indexPattern, [filter.filter], [], esQueryConfig); - set(doc, `aggs.${series.id}.filters.filters.${filter.id}`, builtEsQuery); + overwrite(doc, `aggs.${series.id}.filters.filters.${filter.id}`, builtEsQuery); }); } return next(doc); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js index 3ad00272c66cb..db5a3f50f2e62 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/series/split_by_terms.js @@ -17,7 +17,7 @@ * under the License. */ -import { set } from 'lodash'; +import { overwrite } from '../../helpers'; import { basicAggs } from '../../../../../common/basic_aggs'; import { getBucketsPath } from '../../helpers/get_buckets_path'; import { bucketTransform } from '../../helpers/bucket_transform'; @@ -27,13 +27,13 @@ export function splitByTerms(req, panel, series) { if (series.split_mode === 'terms' && series.terms_field) { const direction = series.terms_direction || 'desc'; const metric = series.metrics.find(item => item.id === series.terms_order_by); - set(doc, `aggs.${series.id}.terms.field`, series.terms_field); - set(doc, `aggs.${series.id}.terms.size`, series.terms_size); + overwrite(doc, `aggs.${series.id}.terms.field`, series.terms_field); + overwrite(doc, `aggs.${series.id}.terms.size`, series.terms_size); if (series.terms_include) { - set(doc, `aggs.${series.id}.terms.include`, series.terms_include); + overwrite(doc, `aggs.${series.id}.terms.include`, series.terms_include); } if (series.terms_exclude) { - set(doc, `aggs.${series.id}.terms.exclude`, series.terms_exclude); + overwrite(doc, `aggs.${series.id}.terms.exclude`, series.terms_exclude); } if (metric && metric.type !== 'count' && ~basicAggs.indexOf(metric.type)) { const sortAggKey = `${series.terms_order_by}-SORT`; @@ -42,12 +42,12 @@ export function splitByTerms(req, panel, series) { series.terms_order_by, sortAggKey ); - set(doc, `aggs.${series.id}.terms.order`, { [bucketPath]: direction }); - set(doc, `aggs.${series.id}.aggs`, { [sortAggKey]: fn(metric) }); + overwrite(doc, `aggs.${series.id}.terms.order`, { [bucketPath]: direction }); + overwrite(doc, `aggs.${series.id}.aggs`, { [sortAggKey]: fn(metric) }); } else if (['_key', '_count'].includes(series.terms_order_by)) { - set(doc, `aggs.${series.id}.terms.order`, { [series.terms_order_by]: direction }); + overwrite(doc, `aggs.${series.id}.terms.order`, { [series.terms_order_by]: direction }); } else { - set(doc, `aggs.${series.id}.terms.order`, { _count: direction }); + overwrite(doc, `aggs.${series.id}.terms.order`, { _count: direction }); } } return next(doc); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js index 6afa434a55085..6b51415627fe9 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/date_histogram.js @@ -17,7 +17,7 @@ * under the License. */ -import { set } from 'lodash'; +import { overwrite } from '../../helpers'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; @@ -41,7 +41,7 @@ export function dateHistogram(req, panel, esQueryConfig, indexPatternObject, cap panel.series.forEach(column => { const aggRoot = calculateAggRoot(doc, column); - set(doc, `${aggRoot}.timeseries.date_histogram`, { + overwrite(doc, `${aggRoot}.timeseries.date_histogram`, { field: timeField, min_doc_count: 0, time_zone: timezone, @@ -52,7 +52,7 @@ export function dateHistogram(req, panel, esQueryConfig, indexPatternObject, cap ...dateHistogramInterval(intervalString), }); - set(doc, aggRoot.replace(/\.aggs$/, '.meta'), { + overwrite(doc, aggRoot.replace(/\.aggs$/, '.meta'), { timeField, intervalString, bucketSize, @@ -64,12 +64,12 @@ export function dateHistogram(req, panel, esQueryConfig, indexPatternObject, cap panel.series.forEach(column => { const aggRoot = calculateAggRoot(doc, column); - set(doc, `${aggRoot}.timeseries.auto_date_histogram`, { + overwrite(doc, `${aggRoot}.timeseries.auto_date_histogram`, { field: timeField, buckets: 1, }); - set(doc, aggRoot.replace(/\.aggs$/, '.meta'), meta); + overwrite(doc, aggRoot.replace(/\.aggs$/, '.meta'), meta); }); }; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js index a05c414f1a311..8bce521e742d8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/filter_ratios.js @@ -19,7 +19,7 @@ const filter = metric => metric.type === 'filter_ratio'; import { bucketTransform } from '../../helpers/bucket_transform'; -import _ from 'lodash'; +import { overwrite } from '../../helpers'; import { calculateAggRoot } from './calculate_agg_root'; export function ratios(req, panel) { @@ -28,10 +28,10 @@ export function ratios(req, panel) { const aggRoot = calculateAggRoot(doc, column); if (column.metrics.some(filter)) { column.metrics.filter(filter).forEach(metric => { - _.set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-numerator.filter`, { + overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}-numerator.filter`, { query_string: { query: metric.numerator || '*', analyze_wildcard: true }, }); - _.set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-denominator.filter`, { + overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}-denominator.filter`, { query_string: { query: metric.denominator || '*', analyze_wildcard: true }, }); @@ -45,13 +45,13 @@ export function ratios(req, panel) { field: metric.field, }), }; - _.set(doc, `${aggRoot}.timeseries.aggs.${metric.id}-numerator.aggs`, aggBody); - _.set(doc, `${aggBody}.timeseries.aggs.${metric.id}-denominator.aggs`, aggBody); + overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}-numerator.aggs`, aggBody); + overwrite(doc, `${aggBody}.timeseries.aggs.${metric.id}-denominator.aggs`, aggBody); numeratorPath = `${metric.id}-numerator>metric`; denominatorPath = `${metric.id}-denominator>metric`; } - _.set(doc, `${aggRoot}.timeseries.aggs.${metric.id}`, { + overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}`, { bucket_script: { buckets_path: { numerator: numeratorPath, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js index 44418efe42dbb..d38282ed3e9aa 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/metric_buckets.js @@ -17,7 +17,7 @@ * under the License. */ -import _ from 'lodash'; +import { overwrite } from '../../helpers'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { bucketTransform } from '../../helpers/bucket_transform'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; @@ -36,7 +36,7 @@ export function metricBuckets(req, panel, esQueryConfig, indexPatternObject) { if (fn) { try { const bucket = fn(metric, column.metrics, intervalString); - _.set(doc, `${aggRoot}.timeseries.aggs.${metric.id}`, bucket); + overwrite(doc, `${aggRoot}.timeseries.aggs.${metric.id}`, bucket); } catch (e) { // meh } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.js index 2b5014a2535dc..c38351e37dc31 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/normalize_query.js @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -const { set, get, isEmpty, forEach } = require('lodash'); - -const isEmptyFilter = (filter = {}) => Boolean(filter.match_all) && isEmpty(filter.match_all); +import _ from 'lodash'; +import { overwrite } from '../../helpers'; +const isEmptyFilter = (filter = {}) => Boolean(filter.match_all) && _.isEmpty(filter.match_all); const hasSiblingPipelineAggregation = (aggs = {}) => Object.keys(aggs).length > 1; /* Last query handler in the chain. You can use this handler @@ -29,26 +29,26 @@ const hasSiblingPipelineAggregation = (aggs = {}) => Object.keys(aggs).length > */ export function normalizeQuery() { return () => doc => { - const series = get(doc, 'aggs.pivot.aggs'); + const series = _.get(doc, 'aggs.pivot.aggs'); const normalizedSeries = {}; - forEach(series, (value, seriesId) => { - const filter = get(value, `filter`); + _.forEach(series, (value, seriesId) => { + const filter = _.get(value, `filter`); if (isEmptyFilter(filter) && !hasSiblingPipelineAggregation(value.aggs)) { - const agg = get(value, 'aggs.timeseries'); + const agg = _.get(value, 'aggs.timeseries'); const meta = { - ...get(value, 'meta'), + ..._.get(value, 'meta'), seriesId, }; - set(normalizedSeries, `${seriesId}`, agg); - set(normalizedSeries, `${seriesId}.meta`, meta); + overwrite(normalizedSeries, `${seriesId}`, agg); + overwrite(normalizedSeries, `${seriesId}.meta`, meta); } else { - set(normalizedSeries, `${seriesId}`, value); + overwrite(normalizedSeries, `${seriesId}`, value); } }); - set(doc, 'aggs.pivot.aggs', normalizedSeries); + overwrite(doc, 'aggs.pivot.aggs', normalizedSeries); return doc; }; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/pivot.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/pivot.js index 972a8c71ed515..6597973c28cf0 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/pivot.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/pivot.js @@ -17,7 +17,8 @@ * under the License. */ -import { get, set, last } from 'lodash'; +import { get, last } from 'lodash'; +import { overwrite } from '../../helpers'; import { basicAggs } from '../../../../../common/basic_aggs'; import { getBucketsPath } from '../../helpers/get_buckets_path'; @@ -27,13 +28,13 @@ export function pivot(req, panel) { return next => doc => { const { sort } = req.payload.state; if (panel.pivot_id) { - set(doc, 'aggs.pivot.terms.field', panel.pivot_id); - set(doc, 'aggs.pivot.terms.size', panel.pivot_rows); + overwrite(doc, 'aggs.pivot.terms.field', panel.pivot_id); + overwrite(doc, 'aggs.pivot.terms.size', panel.pivot_rows); if (sort) { const series = panel.series.find(item => item.id === sort.column); const metric = series && last(series.metrics); if (metric && metric.type === 'count') { - set(doc, 'aggs.pivot.terms.order', { _count: sort.order }); + overwrite(doc, 'aggs.pivot.terms.order', { _count: sort.order }); } else if (metric && basicAggs.includes(metric.type)) { const sortAggKey = `${metric.id}-SORT`; const fn = bucketTransform[metric.type]; @@ -41,16 +42,16 @@ export function pivot(req, panel) { metric.id, sortAggKey ); - set(doc, `aggs.pivot.terms.order`, { [bucketPath]: sort.order }); - set(doc, `aggs.pivot.aggs`, { [sortAggKey]: fn(metric) }); + overwrite(doc, `aggs.pivot.terms.order`, { [bucketPath]: sort.order }); + overwrite(doc, `aggs.pivot.aggs`, { [sortAggKey]: fn(metric) }); } else { - set(doc, 'aggs.pivot.terms.order', { + overwrite(doc, 'aggs.pivot.terms.order', { _key: get(sort, 'order', 'asc'), }); } } } else { - set(doc, 'aggs.pivot.filter.match_all', {}); + overwrite(doc, 'aggs.pivot.filter.match_all', {}); } return next(doc); }; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js index 758da28e93232..b7ffbaa65619c 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/sibling_buckets.js @@ -17,7 +17,7 @@ * under the License. */ -import _ from 'lodash'; +import { overwrite } from '../../helpers'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { bucketTransform } from '../../helpers/bucket_transform'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; @@ -36,7 +36,7 @@ export function siblingBuckets(req, panel, esQueryConfig, indexPatternObject) { if (fn) { try { const bucket = fn(metric, column.metrics, bucketSize); - _.set(doc, `${aggRoot}.${metric.id}`, bucket); + overwrite(doc, `${aggRoot}.${metric.id}`, bucket); } catch (e) { // meh } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.js index 35036abed320f..fd03921346fb8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_everything.js @@ -17,7 +17,7 @@ * under the License. */ -import { set } from 'lodash'; +import { overwrite } from '../../helpers'; import { esQuery } from '../../../../../../data/server'; export function splitByEverything(req, panel, esQueryConfig, indexPattern) { @@ -26,13 +26,13 @@ export function splitByEverything(req, panel, esQueryConfig, indexPattern) { .filter(c => !(c.aggregate_by && c.aggregate_function)) .forEach(column => { if (column.filter) { - set( + overwrite( doc, `aggs.pivot.aggs.${column.id}.filter`, esQuery.buildEsQuery(indexPattern, [column.filter], [], esQueryConfig) ); } else { - set(doc, `aggs.pivot.aggs.${column.id}.filter.match_all`, {}); + overwrite(doc, `aggs.pivot.aggs.${column.id}.filter.match_all`, {}); } }); return next(doc); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.js index 5b7ae735cd50f..a34d53a6bc975 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/request_processors/table/split_by_terms.js @@ -17,7 +17,7 @@ * under the License. */ -import { set } from 'lodash'; +import { overwrite } from '../../helpers'; import { esQuery } from '../../../../../../data/server'; export function splitByTerms(req, panel, esQueryConfig, indexPattern) { @@ -25,11 +25,11 @@ export function splitByTerms(req, panel, esQueryConfig, indexPattern) { panel.series .filter(c => c.aggregate_by && c.aggregate_function) .forEach(column => { - set(doc, `aggs.pivot.aggs.${column.id}.terms.field`, column.aggregate_by); - set(doc, `aggs.pivot.aggs.${column.id}.terms.size`, 100); + overwrite(doc, `aggs.pivot.aggs.${column.id}.terms.field`, column.aggregate_by); + overwrite(doc, `aggs.pivot.aggs.${column.id}.terms.size`, 100); if (column.filter) { - set( + overwrite( doc, `aggs.pivot.aggs.${column.id}.column_filter.filter`, esQuery.buildEsQuery(indexPattern, [column.filter], [], esQueryConfig) From 39e31d61239fea371ad2f97dc12da2302c3c9d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Mon, 4 May 2020 13:29:28 +0200 Subject: [PATCH 073/122] [Logs UI] Add dataset filter to ML module setup screen (#64470) This adds the ability to filter the datasets to be processed by the ML jobs on the setup screen. --- .../log_analysis/validation/datasets.ts | 44 +++ .../http_api/log_analysis/validation/index.ts | 1 + .../common/log_analysis/job_parameters.ts | 60 +++- .../analysis_setup_indices_form.tsx | 113 ++------ .../index_setup_dataset_filter.tsx | 88 ++++++ .../index_setup_row.tsx | 110 ++++++++ .../initial_configuration_step.tsx | 6 +- .../initial_configuration_step/validation.tsx | 19 +- .../log_analysis/api/ml_setup_module_api.ts | 25 +- .../log_analysis/api/validate_datasets.ts | 36 +++ .../logs/log_analysis/log_analysis_module.tsx | 16 +- .../log_analysis/log_analysis_module_types.ts | 16 +- .../log_analysis/log_analysis_setup_state.ts | 264 ++++++++++++++++++ .../log_analysis/log_analysis_setup_state.tsx | 142 ---------- .../log_entry_categories/module_descriptor.ts | 43 ++- .../page_setup_content.tsx | 2 +- .../logs/log_entry_rate/module_descriptor.ts | 36 ++- .../log_entry_rate/page_setup_content.tsx | 2 +- x-pack/plugins/infra/server/infra_server.ts | 2 + .../infra/server/lib/compose/kibana.ts | 1 + .../log_entries_domain/log_entries_domain.ts | 54 +++- .../queries/log_entry_datasets.ts | 98 +++++++ x-pack/plugins/infra/server/plugin.ts | 1 + .../log_analysis/validation/datasets.ts | 69 +++++ .../routes/log_analysis/validation/index.ts | 1 + .../datafeed_log_entry_categories_count.json | 13 +- 26 files changed, 991 insertions(+), 271 deletions(-) create mode 100644 x-pack/plugins/infra/common/http_api/log_analysis/validation/datasets.ts create mode 100644 x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx create mode 100644 x-pack/plugins/infra/public/containers/logs/log_analysis/api/validate_datasets.ts create mode 100644 x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts delete mode 100644 x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx create mode 100644 x-pack/plugins/infra/server/lib/domains/log_entries_domain/queries/log_entry_datasets.ts create mode 100644 x-pack/plugins/infra/server/routes/log_analysis/validation/datasets.ts diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/validation/datasets.ts b/x-pack/plugins/infra/common/http_api/log_analysis/validation/datasets.ts new file mode 100644 index 0000000000000..c9f98ac5fcdea --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_analysis/validation/datasets.ts @@ -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 * as rt from 'io-ts'; + +export const LOG_ANALYSIS_VALIDATE_DATASETS_PATH = + '/api/infra/log_analysis/validation/log_entry_datasets'; + +/** + * Request types + */ +export const validateLogEntryDatasetsRequestPayloadRT = rt.type({ + data: rt.type({ + indices: rt.array(rt.string), + timestampField: rt.string, + startTime: rt.number, + endTime: rt.number, + }), +}); + +export type ValidateLogEntryDatasetsRequestPayload = rt.TypeOf< + typeof validateLogEntryDatasetsRequestPayloadRT +>; + +/** + * Response types + * */ +const logEntryDatasetsEntryRT = rt.strict({ + indexName: rt.string, + datasets: rt.array(rt.string), +}); + +export const validateLogEntryDatasetsResponsePayloadRT = rt.type({ + data: rt.type({ + datasets: rt.array(logEntryDatasetsEntryRT), + }), +}); + +export type ValidateLogEntryDatasetsResponsePayload = rt.TypeOf< + typeof validateLogEntryDatasetsResponsePayloadRT +>; diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/validation/index.ts b/x-pack/plugins/infra/common/http_api/log_analysis/validation/index.ts index f23ef7ee7c302..5f02f5598e6a4 100644 --- a/x-pack/plugins/infra/common/http_api/log_analysis/validation/index.ts +++ b/x-pack/plugins/infra/common/http_api/log_analysis/validation/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './datasets'; export * from './log_entry_rate_indices'; diff --git a/x-pack/plugins/infra/common/log_analysis/job_parameters.ts b/x-pack/plugins/infra/common/log_analysis/job_parameters.ts index 94643e21f1ea6..7e10e45bbae4d 100644 --- a/x-pack/plugins/infra/common/log_analysis/job_parameters.ts +++ b/x-pack/plugins/infra/common/log_analysis/job_parameters.ts @@ -21,17 +21,73 @@ export const getJobId = (spaceId: string, sourceId: string, jobType: string) => export const getDatafeedId = (spaceId: string, sourceId: string, jobType: string) => `datafeed-${getJobId(spaceId, sourceId, jobType)}`; -export const jobSourceConfigurationRT = rt.type({ +export const datasetFilterRT = rt.union([ + rt.strict({ + type: rt.literal('includeAll'), + }), + rt.strict({ + type: rt.literal('includeSome'), + datasets: rt.array(rt.string), + }), +]); + +export type DatasetFilter = rt.TypeOf; + +export const jobSourceConfigurationRT = rt.partial({ indexPattern: rt.string, timestampField: rt.string, bucketSpan: rt.number, + datasetFilter: datasetFilterRT, }); export type JobSourceConfiguration = rt.TypeOf; export const jobCustomSettingsRT = rt.partial({ job_revision: rt.number, - logs_source_config: rt.partial(jobSourceConfigurationRT.props), + logs_source_config: jobSourceConfigurationRT, }); export type JobCustomSettings = rt.TypeOf; + +export const combineDatasetFilters = ( + firstFilter: DatasetFilter, + secondFilter: DatasetFilter +): DatasetFilter => { + if (firstFilter.type === 'includeAll' && secondFilter.type === 'includeAll') { + return { + type: 'includeAll', + }; + } + + const includedDatasets = new Set([ + ...(firstFilter.type === 'includeSome' ? firstFilter.datasets : []), + ...(secondFilter.type === 'includeSome' ? secondFilter.datasets : []), + ]); + + return { + type: 'includeSome', + datasets: [...includedDatasets], + }; +}; + +export const filterDatasetFilter = ( + datasetFilter: DatasetFilter, + predicate: (dataset: string) => boolean +): DatasetFilter => { + if (datasetFilter.type === 'includeAll') { + return datasetFilter; + } else { + const newDatasets = datasetFilter.datasets.filter(predicate); + + if (newDatasets.length > 0) { + return { + type: 'includeSome', + datasets: newDatasets, + }; + } else { + return { + type: 'includeAll', + }; + } + } +}; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx index 649858f657bfe..06dbf5315b83a 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -4,56 +4,41 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCode, EuiDescribedFormGroup, EuiFormRow, EuiCheckbox, EuiToolTip } from '@elastic/eui'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useCallback, useMemo } from 'react'; - +import React, { useCallback } from 'react'; import { LoadingOverlayWrapper } from '../../../loading_overlay_wrapper'; -import { ValidatedIndex, ValidationIndicesUIError } from './validation'; +import { IndexSetupRow } from './index_setup_row'; +import { AvailableIndex } from './validation'; export const AnalysisSetupIndicesForm: React.FunctionComponent<{ disabled?: boolean; - indices: ValidatedIndex[]; + indices: AvailableIndex[]; isValidating: boolean; - onChangeSelectedIndices: (selectedIndices: ValidatedIndex[]) => void; + onChangeSelectedIndices: (selectedIndices: AvailableIndex[]) => void; valid: boolean; }> = ({ disabled = false, indices, isValidating, onChangeSelectedIndices, valid }) => { - const handleCheckboxChange = useCallback( - (event: React.ChangeEvent) => { + const changeIsIndexSelected = useCallback( + (indexName: string, isSelected: boolean) => { onChangeSelectedIndices( indices.map(index => { - const checkbox = event.currentTarget; - return index.name === checkbox.id ? { ...index, isSelected: checkbox.checked } : index; + return index.name === indexName ? { ...index, isSelected } : index; }) ); }, [indices, onChangeSelectedIndices] ); - const choices = useMemo( - () => - indices.map(index => { - const checkbox = ( - {index.name}} - onChange={handleCheckboxChange} - checked={index.validity === 'valid' && index.isSelected} - disabled={disabled || index.validity === 'invalid'} - /> - ); - - return index.validity === 'valid' ? ( - checkbox - ) : ( -
    - {checkbox} -
    - ); - }), - [disabled, handleCheckboxChange, indices] + const changeDatasetFilter = useCallback( + (indexName: string, datasetFilter) => { + onChangeSelectedIndices( + indices.map(index => { + return index.name === indexName ? { ...index, datasetFilter } : index; + }) + ); + }, + [indices, onChangeSelectedIndices] ); return ( @@ -69,13 +54,23 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ description={ } > - <>{choices} + <> + {indices.map(index => ( + + ))} + @@ -85,51 +80,3 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ const indicesSelectionLabel = i18n.translate('xpack.infra.analysisSetup.indicesSelectionLabel', { defaultMessage: 'Indices', }); - -const formatValidationError = (errors: ValidationIndicesUIError[]): React.ReactNode => { - return errors.map(error => { - switch (error.error) { - case 'INDEX_NOT_FOUND': - return ( -

    - {error.index} }} - /> -

    - ); - - case 'FIELD_NOT_FOUND': - return ( -

    - {error.index}, - field: {error.field}, - }} - /> -

    - ); - - case 'FIELD_NOT_VALID': - return ( -

    - {error.index}, - field: {error.field}, - }} - /> -

    - ); - - default: - return ''; - } - }); -}; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx new file mode 100644 index 0000000000000..b37c68f837876 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_dataset_filter.tsx @@ -0,0 +1,88 @@ +/* + * 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 { + EuiFilterButton, + EuiFilterGroup, + EuiPopover, + EuiPopoverTitle, + EuiSelectable, + EuiSelectableOption, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo } from 'react'; +import { DatasetFilter } from '../../../../../common/log_analysis'; +import { useVisibilityState } from '../../../../utils/use_visibility_state'; + +export const IndexSetupDatasetFilter: React.FC<{ + availableDatasets: string[]; + datasetFilter: DatasetFilter; + isDisabled?: boolean; + onChangeDatasetFilter: (datasetFilter: DatasetFilter) => void; +}> = ({ availableDatasets, datasetFilter, isDisabled, onChangeDatasetFilter }) => { + const { isVisible, hide, show } = useVisibilityState(false); + + const changeDatasetFilter = useCallback( + (options: EuiSelectableOption[]) => { + const selectedDatasets = options + .filter(({ checked }) => checked === 'on') + .map(({ label }) => label); + + onChangeDatasetFilter( + selectedDatasets.length === 0 + ? { type: 'includeAll' } + : { type: 'includeSome', datasets: selectedDatasets } + ); + }, + [onChangeDatasetFilter] + ); + + const selectableOptions: EuiSelectableOption[] = useMemo( + () => + availableDatasets.map(datasetName => ({ + label: datasetName, + checked: + datasetFilter.type === 'includeSome' && datasetFilter.datasets.includes(datasetName) + ? 'on' + : undefined, + })), + [availableDatasets, datasetFilter] + ); + + const datasetFilterButton = ( + + + + ); + + return ( + + + + {(list, search) => ( +
    + {search} + {list} +
    + )} +
    +
    +
    + ); +}; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx new file mode 100644 index 0000000000000..18dc2e5aa9bd1 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx @@ -0,0 +1,110 @@ +/* + * 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 { EuiCheckbox, EuiCode, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback } from 'react'; +import { DatasetFilter } from '../../../../../common/log_analysis'; +import { IndexSetupDatasetFilter } from './index_setup_dataset_filter'; +import { AvailableIndex, ValidationIndicesUIError } from './validation'; + +export const IndexSetupRow: React.FC<{ + index: AvailableIndex; + isDisabled: boolean; + onChangeDatasetFilter: (indexName: string, datasetFilter: DatasetFilter) => void; + onChangeIsSelected: (indexName: string, isSelected: boolean) => void; +}> = ({ index, isDisabled, onChangeDatasetFilter, onChangeIsSelected }) => { + const changeIsSelected = useCallback( + (event: React.ChangeEvent) => { + onChangeIsSelected(index.name, event.currentTarget.checked); + }, + [index.name, onChangeIsSelected] + ); + + const changeDatasetFilter = useCallback( + (datasetFilter: DatasetFilter) => onChangeDatasetFilter(index.name, datasetFilter), + [index.name, onChangeDatasetFilter] + ); + + const isSelected = index.validity === 'valid' && index.isSelected; + + return ( + + + {index.name}} + onChange={changeIsSelected} + checked={isSelected} + disabled={isDisabled || index.validity === 'invalid'} + /> + + + {index.validity === 'invalid' ? ( + + + + ) : index.validity === 'valid' ? ( + + ) : null} + + + ); +}; + +const formatValidationError = (errors: ValidationIndicesUIError[]): React.ReactNode => { + return errors.map(error => { + switch (error.error) { + case 'INDEX_NOT_FOUND': + return ( +

    + {error.index} }} + /> +

    + ); + + case 'FIELD_NOT_FOUND': + return ( +

    + {error.index}, + field: {error.field}, + }} + /> +

    + ); + + case 'FIELD_NOT_VALID': + return ( +

    + {error.index}, + field: {error.field}, + }} + /> +

    + ); + + default: + return ''; + } + }); +}; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx index 4ec895dfed4bc..85aa7ce513248 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx @@ -13,7 +13,7 @@ import React, { useMemo } from 'react'; import { SetupStatus } from '../../../../../common/log_analysis'; import { AnalysisSetupIndicesForm } from './analysis_setup_indices_form'; import { AnalysisSetupTimerangeForm } from './analysis_setup_timerange_form'; -import { ValidatedIndex, ValidationIndicesUIError } from './validation'; +import { AvailableIndex, ValidationIndicesUIError } from './validation'; interface InitialConfigurationStepProps { setStartTime: (startTime: number | undefined) => void; @@ -21,9 +21,9 @@ interface InitialConfigurationStepProps { startTime: number | undefined; endTime: number | undefined; isValidating: boolean; - validatedIndices: ValidatedIndex[]; + validatedIndices: AvailableIndex[]; setupStatus: SetupStatus; - setValidatedIndices: (selectedIndices: ValidatedIndex[]) => void; + setValidatedIndices: (selectedIndices: AvailableIndex[]) => void; validationErrors?: ValidationIndicesUIError[]; } diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx index 8b733f66ef4a8..d69e544aeab18 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx @@ -5,22 +5,35 @@ */ import { ValidationIndicesError } from '../../../../../common/http_api'; +import { DatasetFilter } from '../../../../../common/log_analysis'; + +export { ValidationIndicesError }; export type ValidationIndicesUIError = | ValidationIndicesError | { error: 'NETWORK_ERROR' } | { error: 'TOO_FEW_SELECTED_INDICES' }; -interface ValidIndex { +interface ValidAvailableIndex { validity: 'valid'; name: string; isSelected: boolean; + availableDatasets: string[]; + datasetFilter: DatasetFilter; } -interface InvalidIndex { +interface InvalidAvailableIndex { validity: 'invalid'; name: string; errors: ValidationIndicesError[]; } -export type ValidatedIndex = ValidIndex | InvalidIndex; +interface UnvalidatedAvailableIndex { + validity: 'unknown'; + name: string; +} + +export type AvailableIndex = + | ValidAvailableIndex + | InvalidAvailableIndex + | UnvalidatedAvailableIndex; diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts index b1265b389917e..7c8d63374924c 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts @@ -21,7 +21,8 @@ export const callSetupMlModuleAPI = async ( sourceId: string, indexPattern: string, jobOverrides: SetupMlModuleJobOverrides[] = [], - datafeedOverrides: SetupMlModuleDatafeedOverrides[] = [] + datafeedOverrides: SetupMlModuleDatafeedOverrides[] = [], + query?: object ) => { const response = await npStart.http.fetch(`/api/ml/modules/setup/${moduleId}`, { method: 'POST', @@ -34,6 +35,7 @@ export const callSetupMlModuleAPI = async ( startDatafeed: true, jobOverrides, datafeedOverrides, + query, }) ), }); @@ -60,13 +62,20 @@ const setupMlModuleDatafeedOverridesRT = rt.object; export type SetupMlModuleDatafeedOverrides = rt.TypeOf; -const setupMlModuleRequestParamsRT = rt.type({ - indexPatternName: rt.string, - prefix: rt.string, - startDatafeed: rt.boolean, - jobOverrides: rt.array(setupMlModuleJobOverridesRT), - datafeedOverrides: rt.array(setupMlModuleDatafeedOverridesRT), -}); +const setupMlModuleRequestParamsRT = rt.intersection([ + rt.strict({ + indexPatternName: rt.string, + prefix: rt.string, + startDatafeed: rt.boolean, + jobOverrides: rt.array(setupMlModuleJobOverridesRT), + datafeedOverrides: rt.array(setupMlModuleDatafeedOverridesRT), + }), + rt.exact( + rt.partial({ + query: rt.object, + }) + ), +]); const setupMlModuleRequestPayloadRT = rt.intersection([ setupMlModuleTimeParamsRT, diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/validate_datasets.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/validate_datasets.ts new file mode 100644 index 0000000000000..6c9d5e439d359 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/validate_datasets.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + LOG_ANALYSIS_VALIDATE_DATASETS_PATH, + validateLogEntryDatasetsRequestPayloadRT, + validateLogEntryDatasetsResponsePayloadRT, +} from '../../../../../common/http_api'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { npStart } from '../../../../legacy_singletons'; + +export const callValidateDatasetsAPI = async ( + indices: string[], + timestampField: string, + startTime: number, + endTime: number +) => { + const response = await npStart.http.fetch(LOG_ANALYSIS_VALIDATE_DATASETS_PATH, { + method: 'POST', + body: JSON.stringify( + validateLogEntryDatasetsRequestPayloadRT.encode({ + data: { + endTime, + indices, + startTime, + timestampField, + }, + }) + ), + }); + + return decodeOrThrow(validateLogEntryDatasetsResponsePayloadRT)(response); +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx index 99c5a3df7c9b1..cecfea28100ad 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx @@ -5,7 +5,7 @@ */ import { useCallback, useMemo } from 'react'; - +import { DatasetFilter } from '../../../../common/log_analysis'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { useModuleStatus } from './log_analysis_module_status'; import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; @@ -48,10 +48,11 @@ export const useLogAnalysisModule = ({ createPromise: async ( selectedIndices: string[], start: number | undefined, - end: number | undefined + end: number | undefined, + datasetFilter: DatasetFilter ) => { dispatchModuleStatus({ type: 'startedSetup' }); - const setupResult = await moduleDescriptor.setUpModule(start, end, { + const setupResult = await moduleDescriptor.setUpModule(start, end, datasetFilter, { indices: selectedIndices, sourceId, spaceId, @@ -92,11 +93,16 @@ export const useLogAnalysisModule = ({ ]); const cleanUpAndSetUpModule = useCallback( - (selectedIndices: string[], start: number | undefined, end: number | undefined) => { + ( + selectedIndices: string[], + start: number | undefined, + end: number | undefined, + datasetFilter: DatasetFilter + ) => { dispatchModuleStatus({ type: 'startedSetup' }); cleanUpModule() .then(() => { - setUpModule(selectedIndices, start, end); + setUpModule(selectedIndices, start, end, datasetFilter); }) .catch(() => { dispatchModuleStatus({ type: 'failedSetup' }); diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_types.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_types.ts index dc9f25b492635..cc9ef73019844 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_types.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_types.ts @@ -8,7 +8,11 @@ import { DeleteJobsResponsePayload } from './api/ml_cleanup'; import { FetchJobStatusResponsePayload } from './api/ml_get_jobs_summary_api'; import { GetMlModuleResponsePayload } from './api/ml_get_module'; import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; -import { ValidationIndicesResponsePayload } from '../../../../common/http_api/log_analysis'; +import { + ValidationIndicesResponsePayload, + ValidateLogEntryDatasetsResponsePayload, +} from '../../../../common/http_api/log_analysis'; +import { DatasetFilter } from '../../../../common/log_analysis'; export interface ModuleDescriptor { moduleId: string; @@ -20,12 +24,20 @@ export interface ModuleDescriptor { setUpModule: ( start: number | undefined, end: number | undefined, + datasetFilter: DatasetFilter, sourceConfiguration: ModuleSourceConfiguration ) => Promise; cleanUpModule: (spaceId: string, sourceId: string) => Promise; validateSetupIndices: ( - sourceConfiguration: ModuleSourceConfiguration + indices: string[], + timestampField: string ) => Promise; + validateSetupDatasets: ( + indices: string[], + timestampField: string, + startTime: number, + endTime: number + ) => Promise; } export interface ModuleSourceConfiguration { diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts new file mode 100644 index 0000000000000..d46e8bc2485f6 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts @@ -0,0 +1,264 @@ +/* + * 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 { isEqual } from 'lodash'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { usePrevious } from 'react-use'; +import { + combineDatasetFilters, + DatasetFilter, + filterDatasetFilter, + isExampleDataIndex, +} from '../../../../common/log_analysis'; +import { + AvailableIndex, + ValidationIndicesError, + ValidationIndicesUIError, +} from '../../../components/logging/log_analysis_setup/initial_configuration_step'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; + +type SetupHandler = ( + indices: string[], + startTime: number | undefined, + endTime: number | undefined, + datasetFilter: DatasetFilter +) => void; + +interface AnalysisSetupStateArguments { + cleanUpAndSetUpModule: SetupHandler; + moduleDescriptor: ModuleDescriptor; + setUpModule: SetupHandler; + sourceConfiguration: ModuleSourceConfiguration; +} + +const fourWeeksInMs = 86400000 * 7 * 4; + +export const useAnalysisSetupState = ({ + cleanUpAndSetUpModule, + moduleDescriptor: { validateSetupDatasets, validateSetupIndices }, + setUpModule, + sourceConfiguration, +}: AnalysisSetupStateArguments) => { + const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); + const [endTime, setEndTime] = useState(undefined); + + const [validatedIndices, setValidatedIndices] = useState( + sourceConfiguration.indices.map(indexName => ({ + name: indexName, + validity: 'unknown' as const, + })) + ); + + const updateIndicesWithValidationErrors = useCallback( + (validationErrors: ValidationIndicesError[]) => + setValidatedIndices(availableIndices => + availableIndices.map(previousAvailableIndex => { + const indexValiationErrors = validationErrors.filter( + ({ index }) => index === previousAvailableIndex.name + ); + + if (indexValiationErrors.length > 0) { + return { + validity: 'invalid', + name: previousAvailableIndex.name, + errors: indexValiationErrors, + }; + } else if (previousAvailableIndex.validity === 'valid') { + return { + ...previousAvailableIndex, + validity: 'valid', + errors: [], + }; + } else { + return { + validity: 'valid', + name: previousAvailableIndex.name, + isSelected: !isExampleDataIndex(previousAvailableIndex.name), + availableDatasets: [], + datasetFilter: { + type: 'includeAll' as const, + }, + }; + } + }) + ), + [] + ); + + const updateIndicesWithAvailableDatasets = useCallback( + (availableDatasets: Array<{ indexName: string; datasets: string[] }>) => + setValidatedIndices(availableIndices => + availableIndices.map(previousAvailableIndex => { + if (previousAvailableIndex.validity !== 'valid') { + return previousAvailableIndex; + } + + const availableDatasetsForIndex = availableDatasets.filter( + ({ indexName }) => indexName === previousAvailableIndex.name + ); + const newAvailableDatasets = availableDatasetsForIndex.flatMap( + ({ datasets }) => datasets + ); + + // filter out datasets that have disappeared if this index' datasets were updated + const newDatasetFilter: DatasetFilter = + availableDatasetsForIndex.length > 0 + ? filterDatasetFilter(previousAvailableIndex.datasetFilter, dataset => + newAvailableDatasets.includes(dataset) + ) + : previousAvailableIndex.datasetFilter; + + return { + ...previousAvailableIndex, + availableDatasets: newAvailableDatasets, + datasetFilter: newDatasetFilter, + }; + }) + ), + [] + ); + + const validIndexNames = useMemo( + () => validatedIndices.filter(index => index.validity === 'valid').map(index => index.name), + [validatedIndices] + ); + + const selectedIndexNames = useMemo( + () => + validatedIndices + .filter(index => index.validity === 'valid' && index.isSelected) + .map(i => i.name), + [validatedIndices] + ); + + const datasetFilter = useMemo( + () => + validatedIndices + .flatMap(validatedIndex => + validatedIndex.validity === 'valid' + ? validatedIndex.datasetFilter + : { type: 'includeAll' as const } + ) + .reduce(combineDatasetFilters, { type: 'includeAll' as const }), + [validatedIndices] + ); + + const [validateIndicesRequest, validateIndices] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await validateSetupIndices( + sourceConfiguration.indices, + sourceConfiguration.timestampField + ); + }, + onResolve: ({ data: { errors } }) => { + updateIndicesWithValidationErrors(errors); + }, + onReject: () => { + setValidatedIndices([]); + }, + }, + [sourceConfiguration.indices, sourceConfiguration.timestampField] + ); + + const [validateDatasetsRequest, validateDatasets] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + if (validIndexNames.length === 0) { + return { data: { datasets: [] } }; + } + + return await validateSetupDatasets( + validIndexNames, + sourceConfiguration.timestampField, + startTime ?? 0, + endTime ?? Date.now() + ); + }, + onResolve: ({ data: { datasets } }) => { + updateIndicesWithAvailableDatasets(datasets); + }, + }, + [validIndexNames, sourceConfiguration.timestampField, startTime, endTime] + ); + + const setUp = useCallback(() => { + return setUpModule(selectedIndexNames, startTime, endTime, datasetFilter); + }, [setUpModule, selectedIndexNames, startTime, endTime, datasetFilter]); + + const cleanUpAndSetUp = useCallback(() => { + return cleanUpAndSetUpModule(selectedIndexNames, startTime, endTime, datasetFilter); + }, [cleanUpAndSetUpModule, selectedIndexNames, startTime, endTime, datasetFilter]); + + const isValidating = useMemo( + () => validateIndicesRequest.state === 'pending' || validateDatasetsRequest.state === 'pending', + [validateDatasetsRequest.state, validateIndicesRequest.state] + ); + + const validationErrors = useMemo(() => { + if (isValidating) { + return []; + } + + if (validateIndicesRequest.state === 'rejected') { + return [{ error: 'NETWORK_ERROR' }]; + } + + if (selectedIndexNames.length === 0) { + return [{ error: 'TOO_FEW_SELECTED_INDICES' }]; + } + + return validatedIndices.reduce((errors, index) => { + return index.validity === 'invalid' && selectedIndexNames.includes(index.name) + ? [...errors, ...index.errors] + : errors; + }, []); + }, [isValidating, validateIndicesRequest.state, selectedIndexNames, validatedIndices]); + + const prevStartTime = usePrevious(startTime); + const prevEndTime = usePrevious(endTime); + const prevValidIndexNames = usePrevious(validIndexNames); + + useEffect(() => { + validateIndices(); + }, [validateIndices]); + + useEffect(() => { + if ( + startTime !== prevStartTime || + endTime !== prevEndTime || + !isEqual(validIndexNames, prevValidIndexNames) + ) { + validateDatasets(); + } + }, [ + endTime, + prevEndTime, + prevStartTime, + prevValidIndexNames, + startTime, + validIndexNames, + validateDatasets, + ]); + + return { + cleanUpAndSetUp, + datasetFilter, + endTime, + isValidating, + selectedIndexNames, + setEndTime, + setStartTime, + setUp, + startTime, + validatedIndices, + setValidatedIndices, + validationErrors, + }; +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx deleted file mode 100644 index 9f966ed3342e6..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx +++ /dev/null @@ -1,142 +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 { useCallback, useEffect, useMemo, useState } from 'react'; - -import { isExampleDataIndex } from '../../../../common/log_analysis'; -import { - ValidatedIndex, - ValidationIndicesUIError, -} from '../../../components/logging/log_analysis_setup/initial_configuration_step'; -import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; - -type SetupHandler = ( - indices: string[], - startTime: number | undefined, - endTime: number | undefined -) => void; - -interface AnalysisSetupStateArguments { - cleanUpAndSetUpModule: SetupHandler; - moduleDescriptor: ModuleDescriptor; - setUpModule: SetupHandler; - sourceConfiguration: ModuleSourceConfiguration; -} - -const fourWeeksInMs = 86400000 * 7 * 4; - -export const useAnalysisSetupState = ({ - cleanUpAndSetUpModule, - moduleDescriptor: { validateSetupIndices }, - setUpModule, - sourceConfiguration, -}: AnalysisSetupStateArguments) => { - const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); - const [endTime, setEndTime] = useState(undefined); - - const [validatedIndices, setValidatedIndices] = useState([]); - - const [validateIndicesRequest, validateIndices] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - return await validateSetupIndices(sourceConfiguration); - }, - onResolve: ({ data: { errors } }) => { - setValidatedIndices(previousValidatedIndices => - sourceConfiguration.indices.map(indexName => { - const previousValidatedIndex = previousValidatedIndices.filter( - ({ name }) => name === indexName - )[0]; - const indexValiationErrors = errors.filter(({ index }) => index === indexName); - if (indexValiationErrors.length > 0) { - return { - validity: 'invalid', - name: indexName, - errors: indexValiationErrors, - }; - } else { - return { - validity: 'valid', - name: indexName, - isSelected: - previousValidatedIndex?.validity === 'valid' - ? previousValidatedIndex?.isSelected - : !isExampleDataIndex(indexName), - }; - } - }) - ); - }, - onReject: () => { - setValidatedIndices([]); - }, - }, - [sourceConfiguration.indices] - ); - - useEffect(() => { - validateIndices(); - }, [validateIndices]); - - const selectedIndexNames = useMemo( - () => - validatedIndices - .filter(index => index.validity === 'valid' && index.isSelected) - .map(i => i.name), - [validatedIndices] - ); - - const setUp = useCallback(() => { - return setUpModule(selectedIndexNames, startTime, endTime); - }, [setUpModule, selectedIndexNames, startTime, endTime]); - - const cleanUpAndSetUp = useCallback(() => { - return cleanUpAndSetUpModule(selectedIndexNames, startTime, endTime); - }, [cleanUpAndSetUpModule, selectedIndexNames, startTime, endTime]); - - const isValidating = useMemo( - () => - validateIndicesRequest.state === 'pending' || - validateIndicesRequest.state === 'uninitialized', - [validateIndicesRequest.state] - ); - - const validationErrors = useMemo(() => { - if (isValidating) { - return []; - } - - if (validateIndicesRequest.state === 'rejected') { - return [{ error: 'NETWORK_ERROR' }]; - } - - if (selectedIndexNames.length === 0) { - return [{ error: 'TOO_FEW_SELECTED_INDICES' }]; - } - - return validatedIndices.reduce((errors, index) => { - return index.validity === 'invalid' && selectedIndexNames.includes(index.name) - ? [...errors, ...index.errors] - : errors; - }, []); - }, [isValidating, validateIndicesRequest.state, selectedIndexNames, validatedIndices]); - - return { - cleanUpAndSetUp, - endTime, - isValidating, - selectedIndexNames, - setEndTime, - setStartTime, - setUp, - startTime, - validatedIndices, - setValidatedIndices, - validationErrors, - }; -}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/module_descriptor.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/module_descriptor.ts index be7547f2e74cb..45cdd28bd943b 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/module_descriptor.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/module_descriptor.ts @@ -7,20 +7,21 @@ import { bucketSpan, categoriesMessageField, + DatasetFilter, getJobId, LogEntryCategoriesJobType, logEntryCategoriesJobTypes, partitionField, } from '../../../../common/log_analysis'; - import { + cleanUpJobsAndDatafeeds, ModuleDescriptor, ModuleSourceConfiguration, - cleanUpJobsAndDatafeeds, } from '../../../containers/logs/log_analysis'; import { callJobsSummaryAPI } from '../../../containers/logs/log_analysis/api/ml_get_jobs_summary_api'; import { callGetMlModuleAPI } from '../../../containers/logs/log_analysis/api/ml_get_module'; import { callSetupMlModuleAPI } from '../../../containers/logs/log_analysis/api/ml_setup_module_api'; +import { callValidateDatasetsAPI } from '../../../containers/logs/log_analysis/api/validate_datasets'; import { callValidateIndicesAPI } from '../../../containers/logs/log_analysis/api/validate_indices'; const moduleId = 'logs_ui_categories'; @@ -48,6 +49,7 @@ const getModuleDefinition = async () => { const setUpModule = async ( start: number | undefined, end: number | undefined, + datasetFilter: DatasetFilter, { spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration ) => { const indexNamePattern = indices.join(','); @@ -65,10 +67,31 @@ const setUpModule = async ( indexPattern: indexNamePattern, timestampField, bucketSpan, + datasetFilter, }, }, }, ]; + const query = { + bool: { + filter: [ + ...(datasetFilter.type === 'includeSome' + ? [ + { + terms: { + 'event.dataset': datasetFilter.datasets, + }, + }, + ] + : []), + { + exists: { + field: 'message', + }, + }, + ], + }, + }; return callSetupMlModuleAPI( moduleId, @@ -77,7 +100,9 @@ const setUpModule = async ( spaceId, sourceId, indexNamePattern, - jobOverrides + jobOverrides, + [], + query ); }; @@ -85,7 +110,7 @@ const cleanUpModule = async (spaceId: string, sourceId: string) => { return await cleanUpJobsAndDatafeeds(spaceId, sourceId, logEntryCategoriesJobTypes); }; -const validateSetupIndices = async ({ indices, timestampField }: ModuleSourceConfiguration) => { +const validateSetupIndices = async (indices: string[], timestampField: string) => { return await callValidateIndicesAPI(indices, [ { name: timestampField, @@ -102,6 +127,15 @@ const validateSetupIndices = async ({ indices, timestampField }: ModuleSourceCon ]); }; +const validateSetupDatasets = async ( + indices: string[], + timestampField: string, + startTime: number, + endTime: number +) => { + return await callValidateDatasetsAPI(indices, timestampField, startTime, endTime); +}; + export const logEntryCategoriesModule: ModuleDescriptor = { moduleId, jobTypes: logEntryCategoriesJobTypes, @@ -111,5 +145,6 @@ export const logEntryCategoriesModule: ModuleDescriptor { createProcessStep({ cleanUpAndSetUp, errorMessages: lastSetupErrorMessages, - isConfigurationValid: validationErrors.length <= 0, + isConfigurationValid: validationErrors.length <= 0 && !isValidating, setUp, setupStatus, viewResults, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/module_descriptor.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/module_descriptor.ts index 52ba3101dbc38..dfd427138aaa6 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/module_descriptor.ts +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/module_descriptor.ts @@ -6,20 +6,21 @@ import { bucketSpan, + DatasetFilter, getJobId, LogEntryRateJobType, logEntryRateJobTypes, partitionField, } from '../../../../common/log_analysis'; - import { + cleanUpJobsAndDatafeeds, ModuleDescriptor, ModuleSourceConfiguration, - cleanUpJobsAndDatafeeds, } from '../../../containers/logs/log_analysis'; import { callJobsSummaryAPI } from '../../../containers/logs/log_analysis/api/ml_get_jobs_summary_api'; import { callGetMlModuleAPI } from '../../../containers/logs/log_analysis/api/ml_get_module'; import { callSetupMlModuleAPI } from '../../../containers/logs/log_analysis/api/ml_setup_module_api'; +import { callValidateDatasetsAPI } from '../../../containers/logs/log_analysis/api/validate_datasets'; import { callValidateIndicesAPI } from '../../../containers/logs/log_analysis/api/validate_indices'; const moduleId = 'logs_ui_analysis'; @@ -47,6 +48,7 @@ const getModuleDefinition = async () => { const setUpModule = async ( start: number | undefined, end: number | undefined, + datasetFilter: DatasetFilter, { spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration ) => { const indexNamePattern = indices.join(','); @@ -68,6 +70,20 @@ const setUpModule = async ( }, }, ]; + const query = + datasetFilter.type === 'includeSome' + ? { + bool: { + filter: [ + { + terms: { + 'event.dataset': datasetFilter.datasets, + }, + }, + ], + }, + } + : undefined; return callSetupMlModuleAPI( moduleId, @@ -76,7 +92,9 @@ const setUpModule = async ( spaceId, sourceId, indexNamePattern, - jobOverrides + jobOverrides, + [], + query ); }; @@ -84,7 +102,7 @@ const cleanUpModule = async (spaceId: string, sourceId: string) => { return await cleanUpJobsAndDatafeeds(spaceId, sourceId, logEntryRateJobTypes); }; -const validateSetupIndices = async ({ indices, timestampField }: ModuleSourceConfiguration) => { +const validateSetupIndices = async (indices: string[], timestampField: string) => { return await callValidateIndicesAPI(indices, [ { name: timestampField, @@ -97,6 +115,15 @@ const validateSetupIndices = async ({ indices, timestampField }: ModuleSourceCon ]); }; +const validateSetupDatasets = async ( + indices: string[], + timestampField: string, + startTime: number, + endTime: number +) => { + return await callValidateDatasetsAPI(indices, timestampField, startTime, endTime); +}; + export const logEntryRateModule: ModuleDescriptor = { moduleId, jobTypes: logEntryRateJobTypes, @@ -106,5 +133,6 @@ export const logEntryRateModule: ModuleDescriptor = { getModuleDefinition, setUpModule, cleanUpModule, + validateSetupDatasets, validateSetupIndices, }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx index a02dbfa941588..e5c439808115d 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx @@ -55,7 +55,7 @@ export const LogEntryRateSetupContent: React.FunctionComponent = () => { createProcessStep({ cleanUpAndSetUp, errorMessages: lastSetupErrorMessages, - isConfigurationValid: validationErrors.length <= 0, + isConfigurationValid: validationErrors.length <= 0 && !isValidating, setUp, setupStatus, viewResults, diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 4ed30380dc164..06135c6532d77 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -15,6 +15,7 @@ import { initGetLogEntryCategoryDatasetsRoute, initGetLogEntryCategoryExamplesRoute, initGetLogEntryRateRoute, + initValidateLogAnalysisDatasetsRoute, initValidateLogAnalysisIndicesRoute, } from './routes/log_analysis'; import { initMetricExplorerRoute } from './routes/metrics_explorer'; @@ -51,6 +52,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initSnapshotRoute(libs); initNodeDetailsRoute(libs); initSourceRoute(libs); + initValidateLogAnalysisDatasetsRoute(libs); initValidateLogAnalysisIndicesRoute(libs); initLogEntriesRoute(libs); initLogEntriesHighlightsRoute(libs); diff --git a/x-pack/plugins/infra/server/lib/compose/kibana.ts b/x-pack/plugins/infra/server/lib/compose/kibana.ts index d22ca2961cfa5..626b9d46bbde3 100644 --- a/x-pack/plugins/infra/server/lib/compose/kibana.ts +++ b/x-pack/plugins/infra/server/lib/compose/kibana.ts @@ -38,6 +38,7 @@ export function compose(core: CoreSetup, config: InfraConfig, plugins: InfraServ sources, }), logEntries: new InfraLogEntriesDomain(new InfraKibanaLogEntriesAdapter(framework), { + framework, sources, }), metrics: new InfraMetricsDomain(new KibanaMetricsAdapter(framework)), diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index 07bc965dda77a..15bfbce6d512e 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -29,6 +29,14 @@ import { Highlights, compileFormattingRules, } from './message'; +import { KibanaFramework } from '../../adapters/framework/kibana_framework_adapter'; +import { decodeOrThrow } from '../../../../common/runtime_types'; +import { + logEntryDatasetsResponseRT, + LogEntryDatasetBucket, + CompositeDatasetKey, + createLogEntryDatasetsQuery, +} from './queries/log_entry_datasets'; export interface LogEntriesParams { startTimestamp: number; @@ -51,10 +59,15 @@ export const LOG_ENTRIES_PAGE_SIZE = 200; const FIELDS_FROM_CONTEXT = ['log.file.path', 'host.name', 'container.id'] as const; +const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; + export class InfraLogEntriesDomain { constructor( private readonly adapter: LogEntriesAdapter, - private readonly libs: { sources: InfraSources } + private readonly libs: { + framework: KibanaFramework; + sources: InfraSources; + } ) {} public async getLogEntriesAround( @@ -256,6 +269,45 @@ export class InfraLogEntriesDomain { ), }; } + + public async getLogEntryDatasets( + requestContext: RequestHandlerContext, + timestampField: string, + indexName: string, + startTime: number, + endTime: number + ) { + let datasetBuckets: LogEntryDatasetBucket[] = []; + let afterLatestBatchKey: CompositeDatasetKey | undefined; + + while (true) { + const datasetsReponse = await this.libs.framework.callWithRequest( + requestContext, + 'search', + createLogEntryDatasetsQuery( + indexName, + timestampField, + startTime, + endTime, + COMPOSITE_AGGREGATION_BATCH_SIZE, + afterLatestBatchKey + ) + ); + + const { after_key: afterKey, buckets: latestBatchBuckets } = decodeOrThrow( + logEntryDatasetsResponseRT + )(datasetsReponse).aggregations.dataset_buckets; + + datasetBuckets = [...datasetBuckets, ...latestBatchBuckets]; + afterLatestBatchKey = afterKey; + + if (latestBatchBuckets.length < COMPOSITE_AGGREGATION_BATCH_SIZE) { + break; + } + } + + return datasetBuckets.map(({ key: { dataset } }) => dataset); + } } interface LogItemHit { diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/queries/log_entry_datasets.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/queries/log_entry_datasets.ts new file mode 100644 index 0000000000000..1df7072904f68 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/queries/log_entry_datasets.ts @@ -0,0 +1,98 @@ +/* + * 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 rt from 'io-ts'; + +import { commonSearchSuccessResponseFieldsRT } from '../../../../utils/elasticsearch_runtime_types'; + +export const createLogEntryDatasetsQuery = ( + indexName: string, + timestampField: string, + startTime: number, + endTime: number, + size: number, + afterKey?: CompositeDatasetKey +) => ({ + ...defaultRequestParameters, + body: { + query: { + bool: { + filter: [ + { + range: { + [timestampField]: { + gte: startTime, + lte: endTime, + }, + }, + }, + { + exists: { + field: 'event.dataset', + }, + }, + ], + }, + }, + aggs: { + dataset_buckets: { + composite: { + after: afterKey, + size, + sources: [ + { + dataset: { + terms: { + field: 'event.dataset', + order: 'asc', + }, + }, + }, + ], + }, + }, + }, + }, + index: indexName, + size: 0, +}); + +const defaultRequestParameters = { + allowNoIndices: true, + ignoreUnavailable: true, + trackScores: false, + trackTotalHits: false, +}; + +const compositeDatasetKeyRT = rt.type({ + dataset: rt.string, +}); + +export type CompositeDatasetKey = rt.TypeOf; + +const logEntryDatasetBucketRT = rt.type({ + key: compositeDatasetKeyRT, +}); + +export type LogEntryDatasetBucket = rt.TypeOf; + +export const logEntryDatasetsResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.type({ + aggregations: rt.type({ + dataset_buckets: rt.intersection([ + rt.type({ + buckets: rt.array(logEntryDatasetBucketRT), + }), + rt.partial({ + after_key: compositeDatasetKeyRT, + }), + ]), + }), + }), +]); + +export type LogEntryDatasetsResponse = rt.TypeOf; diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index db34033c1d4f8..13446594ab114 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -119,6 +119,7 @@ export class InfraServerPlugin { sources, }), logEntries: new InfraLogEntriesDomain(new InfraKibanaLogEntriesAdapter(framework), { + framework, sources, }), metrics: new InfraMetricsDomain(new KibanaMetricsAdapter(framework)), diff --git a/x-pack/plugins/infra/server/routes/log_analysis/validation/datasets.ts b/x-pack/plugins/infra/server/routes/log_analysis/validation/datasets.ts new file mode 100644 index 0000000000000..d772c000986fc --- /dev/null +++ b/x-pack/plugins/infra/server/routes/log_analysis/validation/datasets.ts @@ -0,0 +1,69 @@ +/* + * 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 Boom from 'boom'; + +import { InfraBackendLibs } from '../../../lib/infra_types'; +import { + LOG_ANALYSIS_VALIDATE_DATASETS_PATH, + validateLogEntryDatasetsRequestPayloadRT, + validateLogEntryDatasetsResponsePayloadRT, +} from '../../../../common/http_api'; + +import { createValidationFunction } from '../../../../common/runtime_types'; + +export const initValidateLogAnalysisDatasetsRoute = ({ + framework, + logEntries, +}: InfraBackendLibs) => { + framework.registerRoute( + { + method: 'post', + path: LOG_ANALYSIS_VALIDATE_DATASETS_PATH, + validate: { + body: createValidationFunction(validateLogEntryDatasetsRequestPayloadRT), + }, + }, + framework.router.handleLegacyErrors(async (requestContext, request, response) => { + try { + const { + data: { indices, timestampField, startTime, endTime }, + } = request.body; + + const datasets = await Promise.all( + indices.map(async indexName => { + const indexDatasets = await logEntries.getLogEntryDatasets( + requestContext, + timestampField, + indexName, + startTime, + endTime + ); + + return { + indexName, + datasets: indexDatasets, + }; + }) + ); + + return response.ok({ + body: validateLogEntryDatasetsResponsePayloadRT.encode({ data: { datasets } }), + }); + } catch (error) { + if (Boom.isBoom(error)) { + throw error; + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + }) + ); +}; diff --git a/x-pack/plugins/infra/server/routes/log_analysis/validation/index.ts b/x-pack/plugins/infra/server/routes/log_analysis/validation/index.ts index 727faca69298e..10c39f9552a3a 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/validation/index.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/validation/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './datasets'; export * from './indices'; diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json index 6e117b4de87ea..2ece259e2bb45 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/logs_ui_categories/ml/datafeed_log_entry_categories_count.json @@ -1,15 +1,4 @@ { "job_id": "JOB_ID", - "indices": ["INDEX_PATTERN_NAME"], - "query": { - "bool": { - "filter": [ - { - "exists": { - "field": "message" - } - } - ] - } - } + "indices": ["INDEX_PATTERN_NAME"] } From 34cccedc8e99d39308b534036df02e15d919fd73 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Mon, 4 May 2020 12:30:31 +0100 Subject: [PATCH 074/122] [Logs UI] [Alerting] Documentation (#64886) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Docs for logs alerts Co-authored-by: Felix Stürmer --- docs/logs/images/alert-actions-menu.png | Bin 0 -> 32575 bytes docs/logs/images/alert-flyout.png | Bin 0 -> 171358 bytes docs/logs/index.asciidoc | 3 +++ docs/logs/logs-alerting.asciidoc | 27 ++++++++++++++++++++++++ 4 files changed, 30 insertions(+) create mode 100644 docs/logs/images/alert-actions-menu.png create mode 100644 docs/logs/images/alert-flyout.png create mode 100644 docs/logs/logs-alerting.asciidoc diff --git a/docs/logs/images/alert-actions-menu.png b/docs/logs/images/alert-actions-menu.png new file mode 100644 index 0000000000000000000000000000000000000000..3f96a700a0ac129d60f1bd3548ddb4b554e2f5b2 GIT binary patch literal 32575 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 Au>b%7 literal 0 HcmV?d00001 diff --git a/docs/logs/images/alert-flyout.png b/docs/logs/images/alert-flyout.png new file mode 100644 index 0000000000000000000000000000000000000000..30c8857758a8b4d39b448e5f4fd892b02270cfc2 GIT binary patch literal 171358 zcmbSzby!u~);A#Ch=8Po2nZVlq&uZSK)R$G>23k(E=fs|?hXSs-CbMh-qH=<;+%8O z<-B*Fe?FgwaIZDz9CP#-^EU}qQh0@hPJ#{x2ZtpsC9Vtyhd2!fha!cF41Ds7+a(7M z4pYiPOiW2yOpH>=!Oqme+5`?xDl}0YMMI^BFjHGu+T0&i3avGiS}OUWXgDS1n+m)* zVbM@lst?n6ufNuYqfQUM!IPLm=+>%CmZ&@pK}t+?t~3x^l}F#$I$gY6Ja20}9ZGe) zQDHk>B!+YP;;w9g{RKm~?5QFl5k)^qT3j!+06Z!s{PV{Mm~LK;3FM@tNZCh_TL!4D zuob=6eQ)pN|NNG!Z*RhY}ne%+C%Y0OV4)| zpI70}klbifQ`KkP+I*s?(#&9&93Y~T5G%oARmmiOQdIZV8eJlN!-q7LS|PO^>qncY zv|Gi;Cuu2E7~x~neGaSF$A%aZhPE}x>T977sbBK*o%6rBk;LUfquu%8L-EPWTGbbX z@K)+`lgmz(QrzbvBCF&zB(Q~-zO17m#|ihF6#TMrZ4v9|L(x-bOYrXRM82Hwn_K8v z2k;G#MFvIF8p}%m~4*GpxOtrDfGJAu`FD2ng+g<7 zC+`d2bAF@w<}Awh>*T3@>`sV>hQUko5WH_MISPIC*=vw_`*2e5>h-7y0*23?sveK z<0Iet_DdTM>fsfa?ua+smCD7%1-AE?#MgDFTSvIhx)7%mXSm^LxOXms`c!@D+wWF` z;Mx^JfkX0U_1DU#vHVK#H~stw_R044WDp>nKI)KA^T= ztysvXMS9Xmar|L4^JlzBAo1`KT5}|=z&&x&;pGjaBi`AlHet^feim=#`X4%eZDGYG z49qtqPQgqIN;F*6z*39m_L{CIzjqfI zqcwieQCJia6m1jrk=)DQP_|}hOL!dS#}-+YP!%*P*C@W3eWZN+`9#&%5`iO}H(5)h zv@kPgF}H1GbhK*pV1y)>cJgEHn@_c`wo4|(N~2$Tf5eXzBUMxxP8zNoi5jsTTO8hW z;`6{2NX&etFIt|Tl`WL*J8Wu>GqGlgQDbJMZoy@)KjKp)Hc~n9c3Wkw&eT}v5g#s%t<_F{xWSu~!K(;_#);%wkL&(mHn50`mC2 z{EK(_{23 z7B8kQUQjzNEt*`MB%UlT(JpDWVX~H->#r!PZZwjuJgy^{DXSDPx?CJwWS+70*UepU z^rZ1D^Zw+WaV>z-B$oaCQ3y$hRh0NEJFqp_IVw3ri4KP@RwZ$a)@*!cOmnAYu+awf z?S~=Uo|unRT)O7W*2!MUyvZSAsbaaogjhCY7Y-={72|LFLNbU?CMLdffStp)7k*uQ ze}%+i?Al^waA%i)E|h5glw z)nk2Kj0459+XeOht|h02&}u=%EQ72W!4NOO3y+Jn^S$HixuH&$IlB>_6LeYUPg)#kjn8KX68>0lx>ya_}Q`A|Kqln$^K|`oYiB7{#8v*9V(rUIfpAzq#glPyE7yKit zh~)64t=oJ=1N(l≠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∈*P`=OiFq!S}$wF0)W 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 * <> * <> +* <> [role="screenshot"] image::logs/images/logs-console.png[Log Console in Kibana] @@ -30,3 +31,5 @@ include::using.asciidoc[] include::configuring.asciidoc[] include::log-rate.asciidoc[] + +include::logs-alerting.asciidoc[] diff --git a/docs/logs/logs-alerting.asciidoc b/docs/logs/logs-alerting.asciidoc new file mode 100644 index 0000000000000..f08a09187a0c8 --- /dev/null +++ b/docs/logs/logs-alerting.asciidoc @@ -0,0 +1,27 @@ +[role="xpack"] +[[xpack-logs-alerting]] +== Logs alerting + +[float] +=== Overview + +To use the alerting functionality you need to {kibana-ref}/alerting-getting-started.html#alerting-setup-prerequisites[set up alerting]. + +You can then select the *Create alert* option, from the *Alerts* actions dropdown. + +[role="screenshot"] +image::logs/images/alert-actions-menu.png[Screenshot showing alerts menu] + +Within the alert flyout you can configure your logs alert: + +[role="screenshot"] +image::logs/images/alert-flyout.png[Screenshot showing alerts flyout] + +[float] +=== Fields and comparators + +The comparators available for conditions depend on the chosen field. The combinations available are: + +- Numeric fields: *more than*, *more than or equals*, *less than*, *less than or equals*, *equals*, and *does not equal*. +- Aggregatable fields: *is* and *is not*. +- Non-aggregatable fields: *matches*, *does not match*, *matches phrase*, *does not match phrase*. From 4c57ebb7cc901b2ef2bf572fc3684eeb615c69c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Mon, 4 May 2020 13:37:54 +0200 Subject: [PATCH 075/122] [Logs UI] Tweak copy in log alerts dialog (#64645) --- .../alerting/logs/expression_editor/document_count.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx index 308165ce08a9b..f80781f5a68d7 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx @@ -56,6 +56,11 @@ export const DocumentCount: React.FC = ({ comparator, value, updateCount, values: { value }, }); + const documentCountSuffix = i18n.translate('xpack.infra.logs.alertFlyout.documentCountSuffix', { + defaultMessage: '{value, plural, one {occurs} other {occur}}', + values: { value }, + }); + return ( @@ -122,6 +127,10 @@ export const DocumentCount: React.FC = ({ comparator, value, updateCount,
    + + + + ); }; From a854a1dd8ff7a02c889a24a6d63271f816178130 Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Mon, 4 May 2020 14:39:11 +0300 Subject: [PATCH 076/122] Migrate tutorial resources (#64298) * Migrate tutorial resources Closes #55710 * Added type to context in apm plugin index * Removed context parameter from ApmPlugin * Generated apm plugin by generate_plugin script * Removed getGreeting declaration * Remove unused assets and comment previewImagePaths * Removed unnecessary types file Co-authored-by: Elastic Machine --- .../azure_metrics/screenshot.png | Bin 1259487 -> 0 bytes .../home/tutorial_resources/logos/azure.svg | 1 - .../home/tutorial_resources/logos/oracle.svg | 1 - .../assets}/activemq_logs/screenshot.png | Bin .../public/assets}/apache_logs/screenshot.png | Bin .../assets}/apache_metrics/screenshot.png | Bin .../public/assets}/auditbeat/screenshot.png | Bin .../public/assets}/aws_logs/screenshot.png | Bin .../public/assets}/aws_metrics/screenshot.png | Bin .../public/assets}/cisco_logs/screenshot.png | Bin .../cockroachdb_metrics/screenshot.png | Bin .../assets}/consul_metrics/screenshot.png | Bin .../assets}/coredns_logs/screenshot.jpg | Bin .../assets}/coredns_metrics/screenshot.png | Bin .../assets}/couchdb_metrics/screenshot.png | Bin .../assets}/docker_metrics/screenshot.png | Bin .../assets}/envoyproxy_logs/screenshot.png | Bin .../public/assets}/ibmmq_logs/screenshot.png | Bin .../assets}/ibmmq_metrics/screenshot.png | Bin .../public/assets}/iis_logs/screenshot.png | Bin .../assets}/iptables_logs/screenshot.png | Bin .../public/assets}/kafka_logs/screenshot.png | Bin .../assets}/kubernetes_metrics/screenshot.png | Bin .../home/public/assets}/logos/activemq.svg | 0 .../home/public/assets}/logos/cisco.svg | 0 .../home/public/assets}/logos/cockroachdb.svg | 0 .../home/public/assets}/logos/consul.svg | 0 .../home/public/assets}/logos/coredns.svg | 0 .../home/public/assets}/logos/couchdb.svg | 0 .../home/public/assets}/logos/envoyproxy.svg | 0 .../home/public/assets}/logos/ibmmq.svg | 0 .../home/public/assets}/logos/iis.svg | 0 .../home/public/assets}/logos/mssql.svg | 0 .../home/public/assets}/logos/munin.svg | 0 .../home/public/assets}/logos/nats.svg | 0 .../home/public/assets}/logos/openmetrics.svg | 0 .../home/public/assets}/logos/stan.svg | 0 .../home/public/assets}/logos/statsd.svg | 0 .../home/public/assets}/logos/suricata.svg | 0 .../home/public/assets}/logos/system.svg | 0 .../home/public/assets}/logos/traefik.svg | 0 .../home/public/assets}/logos/ubiquiti.svg | 0 .../home/public/assets}/logos/uwsgi.svg | 0 .../home/public/assets}/logos/vsphere.svg | 0 .../home/public/assets}/logos/zeek.svg | 0 .../home/public/assets}/logos/zookeeper.svg | 0 .../assets}/logstash_logs/screenshot.png | Bin .../assets}/mongodb_metrics/screenshot.png | Bin .../assets}/mssql_metrics/screenshot.png | Bin .../public/assets}/mysql_logs/screenshot.png | Bin .../assets}/mysql_metrics/screenshot.png | Bin .../public/assets}/nats_logs/screenshot.png | Bin .../assets}/nats_metrics/screenshot.png | Bin .../public/assets}/nginx_logs/screenshot.png | Bin .../assets}/nginx_metrics/screenshot.png | Bin .../assets}/osquery_logs/screenshot.png | Bin .../assets}/postgresql_logs/screenshot.png | Bin .../assets}/rabbitmq_metrics/screenshot.png | Bin .../public/assets}/redis_logs/screenshot.png | Bin .../assets}/redis_metrics/screenshot.png | Bin .../redisenterprise_metrics/screenshot.png | Bin .../assets}/stan_metrics/screenshot.png | Bin .../assets}/suricata_logs/screenshot.png | Bin .../public/assets}/system_logs/screenshot.png | Bin .../assets}/system_metrics/screenshot.png | Bin .../assets}/traefik_logs/screenshot.png | Bin .../assets}/uptime_monitors/screenshot.png | Bin .../assets}/uwsgi_metrics/screenshot.png | Bin .../public/assets}/zeek_logs/screenshot.png | Bin .../server/tutorials/activemq_logs/index.ts | 4 ++-- .../tutorials/activemq_metrics/index.ts | 2 +- .../server/tutorials/apache_logs/index.ts | 2 +- .../server/tutorials/apache_metrics/index.ts | 2 +- .../home/server/tutorials/auditbeat/index.ts | 2 +- .../home/server/tutorials/aws_logs/index.ts | 2 +- .../server/tutorials/aws_metrics/index.ts | 2 +- .../home/server/tutorials/cisco_logs/index.ts | 4 ++-- .../server/tutorials/cloudwatch_logs/index.ts | 1 - .../tutorials/cockroachdb_metrics/index.ts | 4 ++-- .../server/tutorials/consul_metrics/index.ts | 4 ++-- .../server/tutorials/coredns_logs/index.ts | 4 ++-- .../server/tutorials/coredns_metrics/index.ts | 4 ++-- .../server/tutorials/couchdb_metrics/index.ts | 4 ++-- .../server/tutorials/docker_metrics/index.ts | 2 +- .../server/tutorials/envoyproxy_logs/index.ts | 4 ++-- .../tutorials/envoyproxy_metrics/index.ts | 3 +-- .../home/server/tutorials/ibmmq_logs/index.ts | 4 ++-- .../server/tutorials/ibmmq_metrics/index.ts | 4 ++-- .../home/server/tutorials/iis_logs/index.ts | 4 ++-- .../server/tutorials/iptables_logs/index.ts | 4 ++-- .../home/server/tutorials/kafka_logs/index.ts | 2 +- .../tutorials/kubernetes_metrics/index.ts | 2 +- .../server/tutorials/logstash_logs/index.ts | 2 +- .../server/tutorials/mongodb_metrics/index.ts | 2 +- .../server/tutorials/mssql_metrics/index.ts | 4 ++-- .../server/tutorials/munin_metrics/index.ts | 2 +- .../home/server/tutorials/mysql_logs/index.ts | 2 +- .../server/tutorials/mysql_metrics/index.ts | 2 +- .../home/server/tutorials/nats_logs/index.ts | 4 ++-- .../server/tutorials/nats_metrics/index.ts | 4 ++-- .../home/server/tutorials/nginx_logs/index.ts | 2 +- .../server/tutorials/nginx_metrics/index.ts | 2 +- .../tutorials/openmetrics_metrics/index.ts | 2 +- .../server/tutorials/osquery_logs/index.ts | 2 +- .../server/tutorials/php_fpm_metrics/index.ts | 1 - .../server/tutorials/postgresql_logs/index.ts | 2 +- .../tutorials/postgresql_metrics/index.ts | 1 - .../tutorials/rabbitmq_metrics/index.ts | 2 +- .../home/server/tutorials/redis_logs/index.ts | 2 +- .../server/tutorials/redis_metrics/index.ts | 2 +- .../redisenterprise_metrics/index.ts | 3 +-- .../server/tutorials/stan_metrics/index.ts | 4 ++-- .../server/tutorials/statsd_metrics/index.ts | 2 +- .../server/tutorials/suricata_logs/index.ts | 4 ++-- .../server/tutorials/system_logs/index.ts | 4 ++-- .../server/tutorials/system_metrics/index.ts | 4 ++-- .../server/tutorials/traefik_logs/index.ts | 4 ++-- .../server/tutorials/traefik_metrics/index.ts | 3 +-- .../server/tutorials/uptime_monitors/index.ts | 2 +- .../server/tutorials/uwsgi_metrics/index.ts | 4 ++-- .../server/tutorials/vsphere_metrics/index.ts | 3 +-- .../home/server/tutorials/zeek_logs/index.ts | 4 ++-- .../tutorials/zookeeper_metrics/index.ts | 2 +- .../maps/server/tutorials/ems/index.ts | 2 +- .../plugins/apm/public/assets}/apm.png | Bin x-pack/plugins/apm/server/tutorial/index.ts | 2 +- .../public/assets}/boundaries_screenshot.png | Bin 127 files changed, 75 insertions(+), 84 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/public/home/tutorial_resources/azure_metrics/screenshot.png delete mode 100644 src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/azure.svg delete mode 100644 src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/oracle.svg rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/activemq_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/apache_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/apache_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/auditbeat/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/aws_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/aws_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/cisco_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/cockroachdb_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/consul_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/coredns_logs/screenshot.jpg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/coredns_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/couchdb_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/docker_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/envoyproxy_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/ibmmq_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/ibmmq_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/iis_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/iptables_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/kafka_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/kubernetes_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/activemq.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/cisco.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/cockroachdb.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/consul.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/coredns.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/couchdb.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/envoyproxy.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/ibmmq.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/iis.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/mssql.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/munin.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/nats.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/openmetrics.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/stan.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/statsd.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/suricata.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/system.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/traefik.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/ubiquiti.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/uwsgi.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/vsphere.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/zeek.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logos/zookeeper.svg (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/logstash_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/mongodb_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/mssql_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/mysql_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/mysql_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/nats_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/nats_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/nginx_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/nginx_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/osquery_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/postgresql_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/rabbitmq_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/redis_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/redis_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/redisenterprise_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/stan_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/suricata_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/system_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/system_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/traefik_logs/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/uptime_monitors/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/uwsgi_metrics/screenshot.png (100%) rename src/{legacy/core_plugins/kibana/public/home/tutorial_resources => plugins/home/public/assets}/zeek_logs/screenshot.png (100%) rename {src/legacy/core_plugins/kibana/public/home/tutorial_resources/apm => x-pack/plugins/apm/public/assets}/apm.png (100%) rename {src/legacy/core_plugins/kibana/public/home/tutorial_resources/ems => x-pack/plugins/maps/public/assets}/boundaries_screenshot.png (100%) diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/azure_metrics/screenshot.png b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/azure_metrics/screenshot.png deleted file mode 100644 index 22136049b494ad9627683b0b3fff29cd83f0361a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1259487 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

    {nHiKwL|Q~NpZV==ZS7)d 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 z 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 z zUt(}ubwA#vVZL{3yB}57*Jd_@LG`v>;i3YM%9LcyQ3wNqda9hpN zem0kS|4VK3J$R?fwcM<0TTJ%y@`$v`#&$cNsd`LY_gSj*6UrOg*br{tvL^8xnes|# zsOaR2zgg5fX#ReP(<&&%pt-ylz&G+_@`-|Akxj-J@kCZz*hwotSi*9{i{^+hv$3o0K({3496qe$c6S$UM5S4|9N z?=mKoa=aUqPWQ;B$P@Y~X89a{2w#QU&O#jcDyUX!8UP88d!CKpi$h=T&xn!+{68_Ad*}+3FXNw*2-`Q!VHs2E_CDV4u&qOUA641TCzxF1h>LW&!=i_ za6ta|6}>T2`s<>J(6oW^6`v2w8C+fXj0z2fx+u}aOiWBYX~Ltpl2B)*f{- zFw9wJR24Z9ta04E+I@U_T%u*|e^!f;C*g-OR^w7Kdw2CQHqA10DZK`172X`T$`GU( z4nQeu>;A`9@l4F)*Z7oMR`ep9KT0oGV#GbWUHbMEk}Y3@KrTiI5q7-T87r^nySLxX z^wV@FVfIHG2LN1^ssW61Tt#-0k(d?$8^Vqw@b85rv#hoOi zKv6%BZz#d9p5A(Ui@^EEcOrWCb?o+^I{~;aKcdbHl1Rs4Q*^Pa&u8G#gGvAUWA@+o z&2N%-@`sZC!GXlCFNhU!ls$+bJ`A(qSNUk2T3buudG-ri_`36CiZW?Mt8*r)+k8O39@`@c~65*`X&3JMN}0 zc*P;iRX4z9Y}H|h<~iI`qJv}ip#oOSIS{Z2*5)HEA?F^o#hXuV=Faisi`%&i8-;JR ztQ~iAiQhfhTJW^z0g&3iQ4`~Syr^rJ*6FKKZ+ati*}S!xj=H6t@1C66dK_uYhBjol znFdW{3f@x2ugshzPqgs&^NTp2=BgH1!v<^6ZA39sQiC$u0yWkFneQyFQ9a3+sw7M{oAdRQ5f&Q zbPvHCSHvAA$}Ec9k8L-}$e5zc+stPyMS5CF+#P5I)XYK0F4Go%V`r`zRqGtGu<0c? zqI>J%#;pTWF_GVb)YsSds4DR~)G=|+>Fn4EcV4n>l9_FLg-;@$n{J@6^et`RZMW(f z&kR&=ZP$IkJ#ycjy`9)#`sMkc+pB!`!AE7GPbMQd5`V)&(g*wfNDSLIA+R2;|FRNI z$1gV2H#QTx?%`vq^`4@T_0lEPi@wi_mQW4@@H-fY5p<&Z1soL3CTdXLuC` z_CIz+x;4yMzDMtc7HMBQoSW2oR{mGFg#|5ydOSXa)dN8Ac3q)qIs{}kK__b5ZP*_J)JOZaWAf%Qt;jD`A7*O_%TUEr?tWftP2gym+?uV=u`29lDYMWP zvHupMX5r#4(3Oo^Dh8trHg2Fpv|Z`Lb|fIhcNVc*#?ot#Wb^(P%D@hBva}YM!i%6?M^acB6!e#bHlu|WV^Ve&dQLxMwGO+K+F(mqUm+lmR zYteP!(Hno57w^>gnrk(JyM?9)LeL_HL(oIW@ZTFW#gz@!Xs*o~ZQs;V+F(5UQZvZB zsUF*SRPH(~S5bwt+>L1QpoG}ocDMSX4^O+<(?5)V5Xfj(i6FEK((f-juBl0maD8K# zz%>~ac^4q&RPoAKj@lYU_X@jnh2!34dM2zFk7BEP|~k z783KmVaTvihgMp_`y5Q}?reT2*hu(U#eoNi7U>Nwg<^s-%Q8p=M%KvDqwGf;Vvggbha$wuXqHyP+39SCv7Je7WB9Y<>knpmim1 z^n2**A4oH$iqwPzS!WjMc{D?@ZaDdK$TKp$ssrN13MPIKOS zn31kF4WCWk1v<^K_jL(B>ml2;Zg z)SAM!taVPpBFQ~ zjgRZD|7?Dv#k_EZwzClV!qFl<;wk3cU5wDUPkwcD@zyi(5pNV_cIL-DwDh7H?H(0N z;&0{{x;tN>_}yMG;H{Qg9pefBBT+>iMf%5E^oqfz=Yw8EacxJdN0PP4#7+VRxtLXJvBo z_6|vX9_oB>JH@74MaJ=87U9Od8a9eMY3#yF#2j{mrA+H}&({-5AFm|x_cLL=mv=hS zU#XIO$;dY7)FPo{+M4&Y=ZDQIq%N;yu6yNL4c4d)8P~`QB9&a#M*WY<*=@jYMl(k{ ztSbu?qz3o0cpU%QCI0ApnG&j|+ex_eklUgJf^U}y0c8?|P*x@>X1qhWFvyscp8q?s zw@kj6a*~$b=sN4*t*Nb@TV8H^m;?ob8g=eTfDA=Z&3q|;ssg-JfD`uNc;LE=oUOLm z-Fx@YxDC7f!G*o|b=--9n3%ZM%H$jh>aGEde94XIg`#{i?@d^T{Xi#|7@KumwC>Ua zUvc|%RWq`61)2>lx6DqDjFDs+rPC4{lseC20#nB_fnM!g z#!r0q5_L#2Qv1r@xP`n(@pb3VUC-L~O6rB2rlNunTeXv6pV}q5vx7TFmc7y$_;~XQVsJ?7c7b5Vv{iP8n6~ITw ziH`$a;V*`oV~n`r4>AycU<(Jii|hT3ML?orqbLk-{f>o~N^W$AzgR}(#Upr_Yx2pR z?td6kE3yBIgYW)xl7E%eSixP1hn#|Ec{ANEwFS)P#klW7y=|+Ftq=ax@1q4&8K1xc zVH8HTZE!V^Z}*{uP#kG5ZXx5jX1yl`h}aj=V3Z)ia<^DV+IHZeT;%zYIO7(gUqoP! zhi|%I7 zYc~%qjaKO3Y<)>=JX&2{?aVQftN{d2X0pOY7af#iyH|aDe12{1N=O8*JaE5!A%LLi z0QdHrT0_Ml4+i-(*)<7Zg}CgN6*y^xOs`ALZbsOV2v=lnJ`b0`m~A_Y611J;No-hw z7tc#h-KO9NjQT@cGxgTahZwL#MFHEAl1k&YiDx{jkL1}dm%sJQ_JYp^2?G!5`ke;6 z(R#Ghk+)L$+G02vbJ+AN#)DI2!#{-P7TTkjm9wzewQ87G1(qrfJ_QnA>)&H{6i=&R zMk~^5pDX&V<1R8q!Bs!K2$0Dkm(t=|*tZA2c1d{ZfI~dru>BF$Q=lp+q&CcnmOsJJ z<1=;@yMJVyB(s<3mtPnk3$15*DG^U^ zk-xCA3IZ3bum1w_BAYwJPGVlkwRX+3k}lP%mxNwwNtnr2oHs-P9^i&J40vD*@c;(mRe=2RD7!2oY3L}t=ov>jAd%e?j{CjEa@^P=M(9i!v5b{N&e#U=pOE6c*M%E}HJG(ZL zm_WkeNvoi4bLHTG%UJ;o@FmTEmF_YN;s);>Eo@ zHLC1GuDz&SU$SGo^CH9u{rslpO0o>)(TfEvu?IWF*k8=PBW+GpK|iC5NEKC2H~D4> z_V`}hVx7_ggaj}@{q*F62^iiYD1;sTL)E!;BEa-k%UkY1qx0Z&0&acD{1#o8KdBbc zf?dABgD+g5AYI2g!58mw#DMa_d_nHjp+qsc6_DVHBJka)t(l|SDB)TGr!qOVGvDCM zhv2u$LN``}^b7yCqXuNrW>Z+rxb)kaQunVpNJ<>&W=fw3;xp?@8cNy?ZJ13toT#fC zds!t0mWH)&D}DW5m*Bismr=NNGu{;GVi9AskXYzs)&CUq_uUDC=BGr99qP$&*rA2! z+G+deWy8}`i>S;AEZ;jI*J?T5L-DyT#u*y!Ccx2Pr^Dyz$Y_uE8Xs3#N`s4@9vNV~ zR+y;Ar#)j;4lFRinL=b*9(UCTr#7c@DTo-}X|K^a#Cx+p;&I5Fb%KMJ6oDQ%`cYZd zwhMqSe-`R8&e!6R>(4vw632M)(?(qNQ+pG+-f8jPYiNRgmMbV&kDdx{0>y`hhV~`w zUNW8!eqXQu{MfbU!2yipQ5yCe--Z_t3$Si*>p*JG2c^tV&Z`s`-VHwj-WAymDs+CB zV$#z8Ss{cs$#CU~wQt}0@Km$t&i3@v*b=Tc%gbqZ-#rRR{^e;_eV(Mjd)0#Ivii~t z(nXzMl@;H``P1CVKTbO8|5;(vng+W)F2i!3A(FahNVN>8gdJHYr=}htS>^}^hZ)Y}11mF6gNlkeS?`qrQ~U10%^=xp z`8zT4&rDIWeM%~*#sj~dy8Zgjc=soFFhA#j_qf^C#OK7|Bc7A}wQQ{p$>H`urK*3i!oQN^ph4cJxB(!s-8wYr@MH<5ySXd4H>Tc`fcHjkgqAE zvVPvUNoQK#NtZIe8zANW&2&HQOIv3O0KsFGCPXS6uex*;ohd~ ziv>T$br+p%cf7ggM*R`mhi~MFSmb-eg_yXAZt9=J-C(h56uYY+H2B;Q=R|R!+=1Wy z_|&IhD?`6vXK~oe?K7g8K{5cNhT{|Bx}!I`Jnnvjp-VurpJuP3V5kD5tD%G}jMpm_ zQU$pb({5j73^i0#?h?1_BOEAVS&nE(&qJxPU8@tu8Sn@S?->Ee8oC{mGKvu?!k>+= zA4XueJx(~NY|N*TBwMUR0nG`K#sex*5s_IK;at!IfxS6h8@9rswWexECxJxaGrv5y871|;j54Dk&_hT|gyb{7K z`>DO^M7*g++qRRXroGy?lNw#?<)-qDN1KBVvDdW7`!lAOJO^<$KSZBg_6$f}DoXTn zkO>dBs4N(Qua*>sIj6&}$D96}Mcts)VSP*Vn7*eI(=7L6#mB@##(!beMb@cWH;x4U zci>(>>RN|~2|=g0ILr7jU4tVgo>~Hl5}&Lr)UToqV2`!d51!||HPWkVYhg4}fx$s0 zO=5o`S+mlTlJ=wV3tERNRuQ5kIOW2hg>l2OE4T4>339y~^_#ukBwZ`JuHW~bQqacu zN zLuO=s=ct_GWttmROFlcvp1qj)FyS%Fqt~iq9FsXVT4sq0J7+o-hiv@`F}yBnM{MzR z>s`XVyd?QBw+ApO{1enbEP?1h)1BgJ?&d4+Cp~^)iOnnaix$SW*>sH%SO(e~Q-$mp z;f2!bQ5wF0I--}JF?Wt9E`f)PtQCu<_Q4^)BRNQOBJhxz#$~70koe({xxojX;t{<* zmzGnv(<83)UuztvN4jI`QF343j5WUs6d@9vcS_=0dhn0b)+cxZ(sQSoR5bd zxmsrpsB&nXv$n`Q#dNYwO6zwRP0+1c3$Hlt5xQa0j|TWtD9qQ{Q4T-npGErZkE%XP z;A++I8=-28U}+yR0)SnVl~-{+Z)>%Ngb?dM3J+m247!H0BYTiqDAUdVYrK~*@&ufd z@f_NbP(}qY=v+O7#r0Pi6?t_UE(zYg9VnzYX>)R5@!51(?xLJNl1JF>U>z9#QmE~y zh;qMz1hhyaZUXxUb@oa{!7*G8&5xCvL9|AsNEmr9M;{jHjtfrJ>PASfYbM5rOph9m z;X5P?(OK4f8I>`sIY&3kez!rV(|xMn?>%0kVm)r|u?g`;t=Rl=4Ndn|EF#pPp1bn@%fFXv>Hor984P6R$$OqA)SDVmomK%_Qr z=V;FjbF&Jz3yNNq_M#Ar^6@-#O~cx;=ikl(H%p1)V>* z@MX}ln91JnM|>Dri{hRi2tn8ggV7#C^c}~ofzi#%5T|cv>v?Z%=QH@+F}`VD*?Cx< z;qgR8u_jFJ=jvpO`23wG0EfeGO5H5y(;UU!%s!+Zy}-`!bsYTzf^TqV-1?oV>y7); z558ch#DiLLbr+YbF8|2_m@Ma<3qWA3-3c7z%m;ce06(p;G9RPGY2)M0K!4ejE}G)R za6bll{pwT3-0aM@^S#-}w{^1+)4#Y|#UunciApNOdx3{Y(0I%hjkL@$q9^9&ZPlhT-^R_ywi^$j(nC8t}j zXkq_1m2XffHOJSo?bP9)mt<`oWAKqp=`Ghqk7%cK+E~lewD103z8!2!3|DDh9W{!C zfP%n_#0%*x)yLZP7fqpFS__ac*^w$#GCbt6-$bK8J|0)Vin#rVA+h~XI%b%(EnYW7h~2F27{h8)BUP94-R)e^_*qj4?kQz-dnl$EH%8B zSnvhOfu(jswu(A+*hBIN>_6h>T@0m6xqx@=iqwj!g25l4uc)mf{bnCDX25baZ+C-C z;eBLCtB{Zo&0|^@H(V%q0RhohXVcpLy7J2E>Xh|C*KLTP?}%q3LnLFrt<-yW$8ptZ z?DD&3G+qOrLUn%qTEZ_Y*lW*!P_hjTH7+HbH><4@K2DN95U%hGsSg^?-Ty^A;(F6k z9c}D3NKH%YJx>B0PPCe7lvQduUlbcC`3N4YR_*6RG9C8qUHb()WmT%c_pg~`fOE7j z3^X3ODlwrPp0R2FnD>&jo^QIci7DC7<8gc8XOV@7R+SP@TEjByY`fd{l_Lw>q6kg< z8|(8%v-)(Vg;ehlPM(2%_~UlRN-AsuB>5Od~`iguml# zsWVw1tScK$OfDW?8H$Y7aakCE$3NA1f8XFQ3zqcx4e#%dh;}*R4-3{fF<+Mn;;R^9 zx3TUN1#&h(cy)a(!UPj<9l%e5wY_~ydlX91OP?#KdT>Ot0w?U(e-5xE3Q63uoUQ+f zfyeZKaMdBFw|-7kZgxCYxCRH1;%JuoU-LYt$STI}u# z7k74Yd#-)Yi{1pZtP&k7?_G-$Ku&W1SDYX;%3j9DbN%)6W(Fey5VM{``t9SF!!A9} zwilIzgsu}V7kkV`nYZ*ee@fmF(RtUl{HkuO#+J4q-{_r|@wg!tNVfnfx&x{#d!(TH zV}jmfzGk4UcaHa700M|=9$rDn{OA(uHA-pF>9^`WwJ=-C_$2eyKqbt(bbAYQV8M`} z@42d_hxZMVG+x4T)cAD2;xBqQVNxf6i(8;$FHo_R{9GL9J(Rt(T=Mshl@3G9h$rx6jzz zx#CG1sJ5K1Vyj`_eaA0VS&*gMBW|~Rez4Eu%HL0a|AzPpG7P7<7W2kRuM4reKbgbF z0N56~Sv=I(@=?CR3cGE7^@_FesBK*wDC*vLa5Q_yIDOD5bbAVBt3u#OI?WZfyL#F5 z=3mJjF$sO_>RZ)U=Nin$%;3{HZAmQt$*tF}vQ%1G$$?9 zvzyhPpV`duHZbBAI-IA8Iv}4@M&R-UQe4z*iX8_Eu_)5H9enBc+nT}g4U#7mi@y=n zA2hq{NA*h;gys9A8hrU1F|n!*$NO_Ab=u!`6UW&Z+0TE3JgjFaXvX-Eg&X#*e`@4_4T>5E>e1*uKia|k1i7zN#hnRKoH@d^ z)m3yD3S$1~KM)W&BseZ}<&kW9bj|Pht<_BOD^7nX$xbtZ?1o)P4x2xkuIa#Wz#*Ik^%bBbnB461C%%5Y0o6!yQz~^-%C` z5Z+6>&G-K?_m)vr=v}`kAl=<1DN471G}4Hmg3^M3f|MXB-6ADQi3mzbN+aDVA>AoR zm(ncmT)LmM0n zlt{m?{&3`H!U7YSIjKpAr0Z4$L|i8sB4Cygn5ZJ%R)51m#?d1Q>QzBX4DrRC_Y^gs zg7ZT@nAzxUO@;&im{|0&AIvAOxYT*^3O+0Ap1u5u1JGA1dEY%Rx2_?=ofMz9pw!`y znO>=Q&YY}(W_%Ae%NYswr=cR#6+)XLWLUnfHw zFSDV7O5_M^lDax?lJNTTkiZ9X>6#b&5q%>$+vIn$2O@z`D7PK=halCx8~{i^&Q^qQ zGJCF^+>c-|e7#b%eL9F{&wOt~Tq;TB^d5$wBAVtY>XEN`iH*Y#Kp%S!fX3- zeqW#G`No)sV+E|0%s$?~59u{ZXJ8lAknKNeb#Hr#)D(4c?WGz81u5TYOKjjI8-J9V z(r<_*KHvK76-a{La?euV2<~~)f>;8b0htYz`&{S8WR8$b*A73hpTLUf^7=f}f5hut zsLVKgoD!|PM~350+k4N}W`($})H{2P1ixn@-W_+TJnY$J8l;6`YJQg})yHh}{pgex zX0=QSlOgstRu~)3?WHrXN_v?snmVxwPM%18cTUrd547OyHB89u`pe3=U-Ye1?<@ur z9g>LMSRgmD+fkydz9pC@eozlbeboYYl$zxeVUSh^g^)sP8EMgvdWC$i;5(du@bj1; z-(A|zr z1T^f~q0kMK_#c192FdqJ{yW0`XfQ@_qg>UbF@wH95*&{C89t%~AG!KJ`k6n5M<%oM ze%XH}26@>K6 zfC<7&AGn&|O?pxvNX*SY5S16a@5%j^RB~mC1JQimAvm-&yZX_9|CzjAopU?A(wy{T zP7teZ%t%my4Qe;`+0PcT;8jKPp2r(9?1%gN&7mZ$#FA~U!i5|ZeNG*^Oz)+GsrGvk zCDYA85EG0;#-S2iXi%ZWXTgcnQ+ONi5?Xe&FsAqAkBGoTOKE9kwqK813A%w!9Dchie7wWk& zYwLRkoQKtKc%d7-dtRuY=<)N0ZA?yDQq+s19aO*$(8;98C$KAGs8>X8I{ZF-!0}zw zb;~epOq6NICtx}XpnsK`}G)jDVq|0Nruw*?k!$4tE zE;h_y*OOG}Gtc`@;wc&s0!*izoFrth|eWtTVGlMG~ zQZ@1;nch{%c>$VaiUy!gGktKY7P4t~6 z4c*DdyJ?h6LSS-M=D0{dXt`P1@1yaDXMlbha&LhH+p8xP|2b2^ogBf>_i`iO@+93C zdF%68>2ZBNg5hdw&>qg;+Y<7u%zA4*|85MqqW@K=1C61z|NKx6>9SU0I=u&YS@ z4yt03Jormwkp2?*fGhF?t;kJ8L51v!tnB*PpvO6GwnEN7fuFRe|}G zxa3!uQJrD#vmVnvEbeAX;+fwG(fK{S@o0?khKEY9;O#UPrEryp)(}5keUSGU5#P7fJlx4#+3J1Y3rr71 z@m{Ro6Dl9VJ)*lFeMWB#&vIpqv!q4mTKM1=f2x#C4kid#cTg_Uwm4lN1qGq=b|IJNdGJwwVAb#X9+7=QaNF5q?xC0X4G4DV5~h=nX|8A~h|L3czw z#>FIE@@;@E14Q!P;coF=fkR&NL*X9c9{j@%ib4z(U7&!<`Sg z;5U}4&6+HvTFhtxG@``i@a8GCfP2r|2#WoirhOysHJ=QRyBdZ)g${=Wn-)wIm&KNi z6ocGY1S`rS%=kVpcgJsEu>6uuPU z^sVsNJ|%|)w_TOfkMA$r1=1FZl1LFjB3Tc1VZ2-4;yGqNb^K#kS3jAO&EKD{_hEve za3p10EERUg*H?_C&i1RGU-JiubcY^zBJHoStLHbEKAD;SiHx1tagE`=FIcNT1qvf# zVqz4I9!6vHLrW_wEB@Q{9d;TG**2pEV?~Y3*}Qp|mBPu9%*5JgAzz@d3pu)Us}jT~ zPy=JzQkW{kZ)qwrRsSr}l~W}Se?}dZe-3J%;I`dk5T@0L-Yj6=UMrMFGI#y8JNt+amn z4=Ps#Kj0;!x9aO0KfLC6>g~ejT`P)hW$YD$XHF@FLMB9khxzhZ#M1*-t&RK7!H@Lo z;+lRDv+Wz7wz7*(PI^Qj@o%!f=PIzVLsQAHzl73%`#0&p_#dQ0jsyMaHrm*`=U*>` zq+Ceu$te#bGsT84-*u7yD;4-`k>1$@SKh5eE#f|~AlpXcryLdtCFE_=9+1U@wornS zE&M1DW(+0jtF+nh5lQ4n{zpIaM=#(L7$$$|pRR_D%J9dTEnEaW1G$)jYl+Yrc8cBY z`U@w>HGk`XU8v<~0=SSfpMuekG~}H-09Zzj(a23QPX6Wfy47RMV$Szk$x`>V&wJjx z4%_8&wx9l@bCQDHUp_y3h0GjbG+eYlv}#wt=|UA*+7miXN}jfjtrZbl%#Os zTY15n_UNiB_l>W{G1GiVe+FoEE&nq3?~n7q;NJp>MEJce~-w6!!{^c~(!? zL3QSH;muESy(|f)hJ4z`jO$7b502UuBf+)Pyc*;KnLkHtQyPK*c?I>!=ENsbMNQ7 zznY&bmDaO1ELTUjTA)RMsk3F_F>g(r6vj2vow47@RtGwNkK4+QMvIW8DG@@y5dBb+ z8*%=40jXrrw#v~z{VUl0E|YOH7XkO{kPbbsZxp!g3L_DiY0>{Mp@+(Ec7N<+{j%el zj#oM`9-p3O0CDqc*?u@7pna;ci&Aw))@Pt6T2v~v7oE$2 zVZ1SLDT0nnbMNpFi!5PO=bB+9KcJ9(>X!a5HQa7nXCm&6?F$^d)0mP5H#tu(hxtlo z;zCi2<)|Nold`B53&&PgMV6Ud^yG)MDf6M*#`t)9dqco!^ilwFu6Z|cuLXdm*rSBU zA$>1V5 z$|=51W?Z=Q1j~iEiX~v(G^PXEf5va&$p63;y%hf=dO>Gi{D9R+Lm=}14n5GL*``3- z2$LnLKf3GqIt@C?iE@7h{oje<_S0YQUa&+}MulyEq54od1imr?8>*;zcFCW{%=<&N zDwK(uJcvY6LTv1s$iT3?^`P>Omgmztd>i^=Z&Hih#xZ}2IHMkF(rmQufCEGIYZqli z2z?^{Ly`?8g@YzDi*g9&N;hwhjMepUcW?amO_Nk;^!7`i<@aLgi&>G61Msp4(m%YK zz*hpNKWh?We%rqc(l&6XR^pv<{#@PlHXyEV!-5>ziaGxBw+6FtdOj$3_3Nf~in+Sg zL5FNy#1AtoNeai5<4AA41J$jR0Y*RvOh)qVH=@f2wW0HksjtQi{L2ifCGyvu${Ke6 zqmZ2g&(v4%V7~l6oIv5m`wq^R=&Vu*0mo#do$js%2w3Bo(blI5CKvNYH(dhnRXw*# zaeH@%S?=e{f_8WBT8Fa+i8`+Xnv%YWx09b@G#!8Vo2A~*Hx?^hl{OxZZC*5SuxMr=qw&f>z)NY~bu!$>NSIm_(wrR`K03ojBY-6G-E}3>~h4h;ts?x;06# z2&yRJxrcN>0tqQ3(KWFC%c#%UER42Mz26bTs_7b@4uhF8RL=ay27bA6N#+5-G3P>l zwVsE;4=5)k(J?X8;c7tIiLF49X(O8R`iQ2^1l+*YPFKWHjkrPdurH`3|MbK7mAhYZ zq)S~-(P}G)LR<@56)IEeRVYe-ZusGUwUT`zY*UZOxy|~`tQLEW_5yDB1;gH;jsm-l zOeBM3Ce`%wZCy$TmsvTFydK{0;=7hx^+@BlIbA74yt#M8!EE~r%c)vVs=K-O-gw=z zauk)R#>`TC6JB+1pl)`jv2dmO=)uOI$W3nLW<<10p3z2Fy_kxULwldJpYSRJ((Q9z zkM+gp+QS6zlO5V9*Er=Gae~K@PvLsb6L*kyHLJ5n$(k69H1O2H)^ToGJa^A=fXqH(Q|1x|O$q$h*W%o#`#+=w1;}x+Nc1fzYT=%U)bH;(*5td>r_Mnl{RKLnEyOLu?g&u9pL(p zkYD}P)p?XUlxzHUtH$W`S)P}*tsNIQ8uCP3G4z~j;65_aeG1Tu`U1}4WSaXhd91#D z%8{W!3bv6&1+e=eC#O>2Q3dY`yC)Zzoe$6u(Qa;ixZJo7%O5pr`#U8+R(&z65-ZP7 zY|2FjJS4f&ww{O5LbTWSdVaoTr-ha<$N44<3kY>fAN<)aFc1icothI|10&!VO9Gv` zs$r!0=GkjqvLA{}=y`rVpvJ@`yFwy%u<+a;9rKHhW9q~~MPaJIXA=x5ACLH3DJ*wy zMx=OCJ1(*>3D^j2{qbnsW$#WZouiLkA7M0l;C7Y_Nl}J|_zTuFf))+%q$|96ew#CI zt&mKg;z49h2EXmuy~eVzMKEoV>`gF#UXtFm7hmSKBr6NL+mg6A$#3puHNyaE$rP1w z_Li_~wfA;V1tWHjsAils=HB8=iyw7sjI4=Na@d*3be}G8s-%|9tRNoc1}crfrX{sw$c%E35ecHr;(Nfo9n96#tWwgb#eOc zZOzh&&QT|;iw5}H_mNB9P*itJ1^vRO0$&hQt_SV7?OnWT_)^th>LkJVzIabGxUVwn z5G=I#9JGcNc1BCR434)o4HlTNZU)PVcc8q|`D@y_^2f9j)c9{O;D192$H!MWjtL+` z!nfiVGDG53QSotE9m%lAdDoc2xEfN}s}#NW2D#(=GydpO0%0$b9JmCe0PU6U^pJIi zKxUoAA+LYH2d@5q(+hvW0H^~>NRE=^;n-3#geWOtD%?fct)^CS-|ZVv|DR$GANJIYt*$k{W90fD_EoI zm%oG*aTw-l@;B)AThpi8k8V7Fa^Qaogwvz?sRGIiZj4KwIRjO-zm!_S_WM|ro?xk` z2zD)}h!$MxO*u+VtC->mJ8DQJYt0L@koG{nY&eOpt)kc5Qvks#uF!?c{i0i{_@=8J_)D zkex3ySC4vyiiBqEwgM%%&PT>dD)v{;PYcv!R36U zg@txe?*zjSI`j$*2k@wT&W>UfklXn?Nmnhtk=6hS1>TF6XZjN}wJyB(dY>(3ss4+@ z5Q%{1YzDxK*Jpr!Od%k7O6*aM7rsgASf+FX##Fb7cO-7kYSWwU zkbD&7m~aZRh3_xtikTh~i-Dq*Ufcl%lBO0MK25^ip7uk$8w4w5FT=z$Tx2poNzzuV z$KPTxm1ml&z96oSy3sni-NuUVy{M*I^gS1ws0KlODP+d^(&-nDgJyE4r2L&*TAK^> zO>z;K*B17wx z%4_-ka$N>N+0lw*`elDUhQ)3dT5+FRd>i zKo>e=V!ydX#czQ1UrgWWd@^b71pU#;Vv^GUtBcmD`^r=kekQjb=a zmoe?_1Flez`BXWt>STXEDeVdex^nmE6UIuk)ZLpP0ye*xefh<9gxFZ#@8R&AU>7rW z2UIsw+aOvfJXJ09s1q+L2kTDmuUltAF6o{XyiLz!@6J#_>_U&>)3J)3NNZaVPiZ?5 zXvOm~fTIV4L(Lhnq=oKQwm8hL9z*z=%4o^kg08K}a@>ic9or;DTZOzFUg&iApN^#< z2T{ZY;epf)R`<>#g2mhT)PkM(8sso1HhOzmfEvWK4{3E~Ud8L? zdhiK_P(w9LU%-3+@)ul1Wt}HQ%dg*E zuOkp@qf>MTt~$WtzQNBR*afOBomPP^Zv^el7Y_tK{d7fA)NO;(fLpei0>B<=^WEe# z`brK%lGwA%pO>c|oyEHT2`N3JEU_tDM(0o)C`tUa?Q-xH%d)n#*Z3VsloCE^9LR41 zpTaIsMM%bwAmv`ncH?%sB19u|s>Sp2geec*UO%B4*dGv=myPfh*|CRQnw|aq#coxEw?hV#-~x_Sab(9T8U03ziabcMBmC@0Y!d*%@Tc1M;4y z%hxfT<8Xy4R}~wJ*IF8ftbG9eIVrTb`ntn7`E`LMM{jiFb)RiC-myQBTup2;#oJ50Jj z8{50u1_Tk!NvvOY`0{=yhYL9#!>;U)LrPl#ZUKLzZg94kPEqij188TqZ`nM<49*DC zKEK7b^OCX2qcj)C#kEdt7rnf3)K`;)3JafdYCe~H$tdaY1eQ0{OrGsj*e~$!x+f}E!YBn&x#~Q7Xz_VXL8t9Nag8P@Zu{+ z=#R1Z(`8yH)@L@NBJUmegK%5_*{U^=@2};Ph@6Cf7i_Qne|>*HU#8C*jAZ@q`H24~ zqJJyHgsw=6g!-mjUQ{gYA$4n2rn@ z#usJs3L4$^R)5$7a54_uEQbiC2Xn`-C8&T^ zb;^>rS(q0+5@`{Qe5unRxw^MiEgSZ1Ow(_cfB$E}b-$i7Xc^pR7;puB8@m}B2WOx@ z+XHD2WdW_e>F5DaXZr~{=0`j8JG;9v8qJmMEfEP<{Hk~)cKW80V4y}my?T&eR(q80 zoAgB`hBzu>aq+a#VcWpi>GMBXR;t{eQ_QAcE*=k7d(q^lw555RZ%sw<-c&qVSy~DN z5yE~fV8?;yrv#^WN4|I+Xv2gO9rgJ848iZDY6lq!y7S2&Eg`O$X6pmqo;!Nw{sUEP zB_q<$WMwEY;Jz>3N*)wAl;Ai_GzGT>Z{& zd^1NUqoF0C<>`A9dwQ9ow;@l2fBM~d8lC#OIZtcn(6rf2z`Sh}$@|se(?o?Vxb$8?KsWKHu z!ioitN(wIRsp&Mx(5dd8-_q%-;5&RII{xkX>uHPX)w(LC_(M1rCbU0WF|L|)tc>Aw z0Qmw<;8NGO_2NQ{jStcLgw5Xm?FJSOX%>DV&2S>~hxjugn)168s;}Sz0djD+tOLh$ z(A=D9a_I6mf29v*KxAu|+uk3*2AL=-prlme++1#3^rUlmCepQwhSB)N-usPnDT1#v zI6hCZ+8}ZW$uWk|QEOLbfoexpT?lNAdv{NtGtLSVLBL6vPnASYhT?O6LIGY&0#M12 zwKjTo9?1tm{fBKNU8|RFK3u%3G_BZIf&Mmc<`P&|0wK{XY)ylLjT{o0ug-nlEUZWP zgN`EHuxW-qD0LK*`su)eLWIA{#ENApSM;y^=TTwq4uW$T0?CGQec~M{AL;8M z<`{)kC*dtDsn6&_H8x{ZWr|%fdI;89GRbAk1gT{%SO&|;hdS!>Z8)2gf^Mc)%d^|a zB{K?@(U%XldxPqBj$;9C>?*t~QjcZ{!=l?yTxXhF$Y;_}MjneSvK<#hs_?8*>4r$a zMBeG8lhutGz`1GVp@i4s@tzQTD-xU zeYPvN%P$XZk{H)}6M$Z)lrwPpY1Ebh#xsuRrjBv3B6o9if>`a%QSs62^7wQ!BlO`f}qf;j5%WKUGY;grdhT0okMj7v(h8mCyFZCq8UX7%D z9;gxWW*;-`>(g-&>hL!?e8oy}POA}SWM{-OBQJjaWaM`opa@;1i68&P($QHDNnO5C zQBl@vkQnU`0W(6Ah)q1Rfl@||C~lhuAAm*x?+T{xudA&&dJ{L{Ca+9(#8Eyk%wjC#)=rLBMXp%G<`T6k+m$_2>D z0qukB**+42Yu{vU6JuRt^|8;Ipq0Rc3R6MjD)QjA$NRGVVg8o|n_aVu^9XaNLR5q~z1#kn}Ir20OK# z`=!>m#hsS0Wp3PWU%8m^(^VFgf4iXs@_MY-QZzTI$M zO1<%xUw<;I{!1|vbNTmeR&&s(dn~WcxtD+|Yi@lV&wX!6AfIx*USa+;Yz?KpPSCS- zgRF7-8r9$%Y>!?U_Wjajb#Z-#(6xW~WjA!&3<=%uy|IOHMqQSV^W;s^xPS2FQFZ;a z5p$#;owsLR5NC9M6{Sd@CD8CZ*!6c%c5dHn@2|Aw=Pa_vR-C>=Q@rR`E;jt5XeUEa zex$r=xGi)SsvCkEaUZfm&5EKaf7_7v-RtE&%Knl9Wj>C>EK-37#ali1u}u_N&8!YX z8WiwL?)b&{kA?c?uB5`hqOCN|@}K>mHynkeU1g4wbqrj%7L

    01>= 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

    <@Mg5Pw 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 Ub?1Ls1U?Qpx$Y(HK6T}P0bI7e%m4rY diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/azure.svg b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/azure.svg deleted file mode 100644 index a93c83b4b4ae0..0000000000000 --- a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/azure.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/oracle.svg b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/oracle.svg deleted file mode 100644 index 78db57f914818..0000000000000 --- a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/oracle.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/activemq_logs/screenshot.png b/src/plugins/home/public/assets/activemq_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/activemq_logs/screenshot.png rename to src/plugins/home/public/assets/activemq_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/apache_logs/screenshot.png b/src/plugins/home/public/assets/apache_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/apache_logs/screenshot.png rename to src/plugins/home/public/assets/apache_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/apache_metrics/screenshot.png b/src/plugins/home/public/assets/apache_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/apache_metrics/screenshot.png rename to src/plugins/home/public/assets/apache_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/auditbeat/screenshot.png b/src/plugins/home/public/assets/auditbeat/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/auditbeat/screenshot.png rename to src/plugins/home/public/assets/auditbeat/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/aws_logs/screenshot.png b/src/plugins/home/public/assets/aws_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/aws_logs/screenshot.png rename to src/plugins/home/public/assets/aws_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/aws_metrics/screenshot.png b/src/plugins/home/public/assets/aws_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/aws_metrics/screenshot.png rename to src/plugins/home/public/assets/aws_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/cisco_logs/screenshot.png b/src/plugins/home/public/assets/cisco_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/cisco_logs/screenshot.png rename to src/plugins/home/public/assets/cisco_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/cockroachdb_metrics/screenshot.png b/src/plugins/home/public/assets/cockroachdb_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/cockroachdb_metrics/screenshot.png rename to src/plugins/home/public/assets/cockroachdb_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/consul_metrics/screenshot.png b/src/plugins/home/public/assets/consul_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/consul_metrics/screenshot.png rename to src/plugins/home/public/assets/consul_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/coredns_logs/screenshot.jpg b/src/plugins/home/public/assets/coredns_logs/screenshot.jpg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/coredns_logs/screenshot.jpg rename to src/plugins/home/public/assets/coredns_logs/screenshot.jpg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/coredns_metrics/screenshot.png b/src/plugins/home/public/assets/coredns_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/coredns_metrics/screenshot.png rename to src/plugins/home/public/assets/coredns_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/couchdb_metrics/screenshot.png b/src/plugins/home/public/assets/couchdb_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/couchdb_metrics/screenshot.png rename to src/plugins/home/public/assets/couchdb_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/docker_metrics/screenshot.png b/src/plugins/home/public/assets/docker_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/docker_metrics/screenshot.png rename to src/plugins/home/public/assets/docker_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/envoyproxy_logs/screenshot.png b/src/plugins/home/public/assets/envoyproxy_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/envoyproxy_logs/screenshot.png rename to src/plugins/home/public/assets/envoyproxy_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/ibmmq_logs/screenshot.png b/src/plugins/home/public/assets/ibmmq_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/ibmmq_logs/screenshot.png rename to src/plugins/home/public/assets/ibmmq_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/ibmmq_metrics/screenshot.png b/src/plugins/home/public/assets/ibmmq_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/ibmmq_metrics/screenshot.png rename to src/plugins/home/public/assets/ibmmq_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/iis_logs/screenshot.png b/src/plugins/home/public/assets/iis_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/iis_logs/screenshot.png rename to src/plugins/home/public/assets/iis_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/iptables_logs/screenshot.png b/src/plugins/home/public/assets/iptables_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/iptables_logs/screenshot.png rename to src/plugins/home/public/assets/iptables_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/kafka_logs/screenshot.png b/src/plugins/home/public/assets/kafka_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/kafka_logs/screenshot.png rename to src/plugins/home/public/assets/kafka_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/kubernetes_metrics/screenshot.png b/src/plugins/home/public/assets/kubernetes_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/kubernetes_metrics/screenshot.png rename to src/plugins/home/public/assets/kubernetes_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg b/src/plugins/home/public/assets/logos/activemq.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/activemq.svg rename to src/plugins/home/public/assets/logos/activemq.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/cisco.svg b/src/plugins/home/public/assets/logos/cisco.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/cisco.svg rename to src/plugins/home/public/assets/logos/cisco.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/cockroachdb.svg b/src/plugins/home/public/assets/logos/cockroachdb.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/cockroachdb.svg rename to src/plugins/home/public/assets/logos/cockroachdb.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/consul.svg b/src/plugins/home/public/assets/logos/consul.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/consul.svg rename to src/plugins/home/public/assets/logos/consul.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/coredns.svg b/src/plugins/home/public/assets/logos/coredns.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/coredns.svg rename to src/plugins/home/public/assets/logos/coredns.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/couchdb.svg b/src/plugins/home/public/assets/logos/couchdb.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/couchdb.svg rename to src/plugins/home/public/assets/logos/couchdb.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/envoyproxy.svg b/src/plugins/home/public/assets/logos/envoyproxy.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/envoyproxy.svg rename to src/plugins/home/public/assets/logos/envoyproxy.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/ibmmq.svg b/src/plugins/home/public/assets/logos/ibmmq.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/ibmmq.svg rename to src/plugins/home/public/assets/logos/ibmmq.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/iis.svg b/src/plugins/home/public/assets/logos/iis.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/iis.svg rename to src/plugins/home/public/assets/logos/iis.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/mssql.svg b/src/plugins/home/public/assets/logos/mssql.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/mssql.svg rename to src/plugins/home/public/assets/logos/mssql.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/munin.svg b/src/plugins/home/public/assets/logos/munin.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/munin.svg rename to src/plugins/home/public/assets/logos/munin.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/nats.svg b/src/plugins/home/public/assets/logos/nats.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/nats.svg rename to src/plugins/home/public/assets/logos/nats.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/openmetrics.svg b/src/plugins/home/public/assets/logos/openmetrics.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/openmetrics.svg rename to src/plugins/home/public/assets/logos/openmetrics.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/stan.svg b/src/plugins/home/public/assets/logos/stan.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/stan.svg rename to src/plugins/home/public/assets/logos/stan.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/statsd.svg b/src/plugins/home/public/assets/logos/statsd.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/statsd.svg rename to src/plugins/home/public/assets/logos/statsd.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/suricata.svg b/src/plugins/home/public/assets/logos/suricata.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/suricata.svg rename to src/plugins/home/public/assets/logos/suricata.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/system.svg b/src/plugins/home/public/assets/logos/system.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/system.svg rename to src/plugins/home/public/assets/logos/system.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/traefik.svg b/src/plugins/home/public/assets/logos/traefik.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/traefik.svg rename to src/plugins/home/public/assets/logos/traefik.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/ubiquiti.svg b/src/plugins/home/public/assets/logos/ubiquiti.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/ubiquiti.svg rename to src/plugins/home/public/assets/logos/ubiquiti.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/uwsgi.svg b/src/plugins/home/public/assets/logos/uwsgi.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/uwsgi.svg rename to src/plugins/home/public/assets/logos/uwsgi.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/vsphere.svg b/src/plugins/home/public/assets/logos/vsphere.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/vsphere.svg rename to src/plugins/home/public/assets/logos/vsphere.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/zeek.svg b/src/plugins/home/public/assets/logos/zeek.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/zeek.svg rename to src/plugins/home/public/assets/logos/zeek.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/zookeeper.svg b/src/plugins/home/public/assets/logos/zookeeper.svg similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/zookeeper.svg rename to src/plugins/home/public/assets/logos/zookeeper.svg diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logstash_logs/screenshot.png b/src/plugins/home/public/assets/logstash_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/logstash_logs/screenshot.png rename to src/plugins/home/public/assets/logstash_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/mongodb_metrics/screenshot.png b/src/plugins/home/public/assets/mongodb_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/mongodb_metrics/screenshot.png rename to src/plugins/home/public/assets/mongodb_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/mssql_metrics/screenshot.png b/src/plugins/home/public/assets/mssql_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/mssql_metrics/screenshot.png rename to src/plugins/home/public/assets/mssql_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/mysql_logs/screenshot.png b/src/plugins/home/public/assets/mysql_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/mysql_logs/screenshot.png rename to src/plugins/home/public/assets/mysql_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/mysql_metrics/screenshot.png b/src/plugins/home/public/assets/mysql_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/mysql_metrics/screenshot.png rename to src/plugins/home/public/assets/mysql_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/nats_logs/screenshot.png b/src/plugins/home/public/assets/nats_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/nats_logs/screenshot.png rename to src/plugins/home/public/assets/nats_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/nats_metrics/screenshot.png b/src/plugins/home/public/assets/nats_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/nats_metrics/screenshot.png rename to src/plugins/home/public/assets/nats_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/nginx_logs/screenshot.png b/src/plugins/home/public/assets/nginx_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/nginx_logs/screenshot.png rename to src/plugins/home/public/assets/nginx_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/nginx_metrics/screenshot.png b/src/plugins/home/public/assets/nginx_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/nginx_metrics/screenshot.png rename to src/plugins/home/public/assets/nginx_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/osquery_logs/screenshot.png b/src/plugins/home/public/assets/osquery_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/osquery_logs/screenshot.png rename to src/plugins/home/public/assets/osquery_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/postgresql_logs/screenshot.png b/src/plugins/home/public/assets/postgresql_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/postgresql_logs/screenshot.png rename to src/plugins/home/public/assets/postgresql_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/rabbitmq_metrics/screenshot.png b/src/plugins/home/public/assets/rabbitmq_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/rabbitmq_metrics/screenshot.png rename to src/plugins/home/public/assets/rabbitmq_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/redis_logs/screenshot.png b/src/plugins/home/public/assets/redis_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/redis_logs/screenshot.png rename to src/plugins/home/public/assets/redis_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/redis_metrics/screenshot.png b/src/plugins/home/public/assets/redis_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/redis_metrics/screenshot.png rename to src/plugins/home/public/assets/redis_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/redisenterprise_metrics/screenshot.png b/src/plugins/home/public/assets/redisenterprise_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/redisenterprise_metrics/screenshot.png rename to src/plugins/home/public/assets/redisenterprise_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/stan_metrics/screenshot.png b/src/plugins/home/public/assets/stan_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/stan_metrics/screenshot.png rename to src/plugins/home/public/assets/stan_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/suricata_logs/screenshot.png b/src/plugins/home/public/assets/suricata_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/suricata_logs/screenshot.png rename to src/plugins/home/public/assets/suricata_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/system_logs/screenshot.png b/src/plugins/home/public/assets/system_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/system_logs/screenshot.png rename to src/plugins/home/public/assets/system_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/system_metrics/screenshot.png b/src/plugins/home/public/assets/system_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/system_metrics/screenshot.png rename to src/plugins/home/public/assets/system_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/traefik_logs/screenshot.png b/src/plugins/home/public/assets/traefik_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/traefik_logs/screenshot.png rename to src/plugins/home/public/assets/traefik_logs/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/uptime_monitors/screenshot.png b/src/plugins/home/public/assets/uptime_monitors/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/uptime_monitors/screenshot.png rename to src/plugins/home/public/assets/uptime_monitors/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/uwsgi_metrics/screenshot.png b/src/plugins/home/public/assets/uwsgi_metrics/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/uwsgi_metrics/screenshot.png rename to src/plugins/home/public/assets/uwsgi_metrics/screenshot.png diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/zeek_logs/screenshot.png b/src/plugins/home/public/assets/zeek_logs/screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/zeek_logs/screenshot.png rename to src/plugins/home/public/assets/zeek_logs/screenshot.png diff --git a/src/plugins/home/server/tutorials/activemq_logs/index.ts b/src/plugins/home/server/tutorials/activemq_logs/index.ts index 6511a21b15c44..e85100996d4a1 100644 --- a/src/plugins/home/server/tutorials/activemq_logs/index.ts +++ b/src/plugins/home/server/tutorials/activemq_logs/index.ts @@ -48,7 +48,7 @@ export function activemqLogsSpecProvider(context: TutorialContext): TutorialSche learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-activemq.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/activemq.svg', + euiIconType: '/plugins/home/assets/logos/activemq.svg', artifacts: { dashboards: [ { @@ -64,7 +64,7 @@ export function activemqLogsSpecProvider(context: TutorialContext): TutorialSche }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/activemq_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/activemq_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/activemq_metrics/index.ts b/src/plugins/home/server/tutorials/activemq_metrics/index.ts index 3898e2b5338b1..b477e65017ed3 100644 --- a/src/plugins/home/server/tutorials/activemq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/activemq_metrics/index.ts @@ -48,7 +48,7 @@ export function activemqMetricsSpecProvider(context: TutorialContext): TutorialS learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-activemq.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/activemq.svg', + euiIconType: '/plugins/home/assets/logos/activemq.svg', isBeta: true, artifacts: { application: { diff --git a/src/plugins/home/server/tutorials/apache_logs/index.ts b/src/plugins/home/server/tutorials/apache_logs/index.ts index adf94f5567096..434f0b0b83f98 100644 --- a/src/plugins/home/server/tutorials/apache_logs/index.ts +++ b/src/plugins/home/server/tutorials/apache_logs/index.ts @@ -65,7 +65,7 @@ export function apacheLogsSpecProvider(context: TutorialContext): TutorialSchema }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/apache_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/apache_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/apache_metrics/index.ts b/src/plugins/home/server/tutorials/apache_metrics/index.ts index e272f3efb5abe..1521c9820c400 100644 --- a/src/plugins/home/server/tutorials/apache_metrics/index.ts +++ b/src/plugins/home/server/tutorials/apache_metrics/index.ts @@ -64,7 +64,7 @@ export function apacheMetricsSpecProvider(context: TutorialContext): TutorialSch }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/apache_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/apache_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/auditbeat/index.ts b/src/plugins/home/server/tutorials/auditbeat/index.ts index 6d94e7507ff42..dadbf913d5ed5 100644 --- a/src/plugins/home/server/tutorials/auditbeat/index.ts +++ b/src/plugins/home/server/tutorials/auditbeat/index.ts @@ -63,7 +63,7 @@ processes, users, logins, sockets information, file accesses, and more. \ }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/auditbeat/screenshot.png', + previewImagePath: '/plugins/home/assets/auditbeat/screenshot.png', onPrem: onPremInstructions(platforms, context), elasticCloud: cloudInstructions(platforms), onPremElasticCloud: onPremCloudInstructions(platforms), diff --git a/src/plugins/home/server/tutorials/aws_logs/index.ts b/src/plugins/home/server/tutorials/aws_logs/index.ts index 8908838bd558a..2fa22fa2c2d70 100644 --- a/src/plugins/home/server/tutorials/aws_logs/index.ts +++ b/src/plugins/home/server/tutorials/aws_logs/index.ts @@ -65,7 +65,7 @@ export function awsLogsSpecProvider(context: TutorialContext): TutorialSchema { }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/aws_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/aws_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/aws_metrics/index.ts b/src/plugins/home/server/tutorials/aws_metrics/index.ts index d00951b524530..c52620e150b5f 100644 --- a/src/plugins/home/server/tutorials/aws_metrics/index.ts +++ b/src/plugins/home/server/tutorials/aws_metrics/index.ts @@ -66,7 +66,7 @@ export function awsMetricsSpecProvider(context: TutorialContext): TutorialSchema }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/aws_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/aws_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/cisco_logs/index.ts b/src/plugins/home/server/tutorials/cisco_logs/index.ts index a694802663171..4514b61570b07 100644 --- a/src/plugins/home/server/tutorials/cisco_logs/index.ts +++ b/src/plugins/home/server/tutorials/cisco_logs/index.ts @@ -50,7 +50,7 @@ supports the "asa" fileset for Cisco ASA firewall logs received over syslog or r learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-cisco.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/cisco.svg', + euiIconType: '/plugins/home/assets/logos/cisco.svg', artifacts: { dashboards: [], application: { @@ -64,7 +64,7 @@ supports the "asa" fileset for Cisco ASA firewall logs received over syslog or r }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/cisco_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/cisco_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts index 10f0eb3e4f34f..9d33d9bf786d0 100644 --- a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts +++ b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts @@ -58,7 +58,6 @@ export function cloudwatchLogsSpecProvider(context: TutorialContext): TutorialSc }, }, completionTimeMinutes: 10, - // previewImagePath: '/plugins/kibana/home/tutorial_resources/uptime_monitors/screenshot.png', onPrem: onPremInstructions([], context), elasticCloud: cloudInstructions(), onPremElasticCloud: onPremCloudInstructions(), diff --git a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts index a8146e024a37e..96c02f24e347a 100644 --- a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts @@ -48,7 +48,7 @@ export function cockroachdbMetricsSpecProvider(context: TutorialContext): Tutori learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-cockroachdb.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/cockroachdb.svg', + euiIconType: '/plugins/home/assets/logos/cockroachdb.svg', artifacts: { dashboards: [ { @@ -67,7 +67,7 @@ export function cockroachdbMetricsSpecProvider(context: TutorialContext): Tutori }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/cockroachdb_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/cockroachdb_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/consul_metrics/index.ts b/src/plugins/home/server/tutorials/consul_metrics/index.ts index 8b12f38274ee9..8bf4333cb018f 100644 --- a/src/plugins/home/server/tutorials/consul_metrics/index.ts +++ b/src/plugins/home/server/tutorials/consul_metrics/index.ts @@ -48,7 +48,7 @@ export function consulMetricsSpecProvider(context: TutorialContext): TutorialSch learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-consul.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/consul.svg', + euiIconType: '/plugins/home/assets/logos/consul.svg', artifacts: { dashboards: [ { @@ -64,7 +64,7 @@ export function consulMetricsSpecProvider(context: TutorialContext): TutorialSch }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/consul_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/consul_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/coredns_logs/index.ts b/src/plugins/home/server/tutorials/coredns_logs/index.ts index e2f976c0f377b..1c62366251661 100644 --- a/src/plugins/home/server/tutorials/coredns_logs/index.ts +++ b/src/plugins/home/server/tutorials/coredns_logs/index.ts @@ -50,7 +50,7 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-coredns.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/coredns.svg', + euiIconType: '/plugins/home/assets/logos/coredns.svg', artifacts: { dashboards: [ { @@ -66,7 +66,7 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/coredns_logs/screenshot.jpg', + previewImagePath: '/plugins/home/assets/coredns_logs/screenshot.jpg', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/coredns_metrics/index.ts b/src/plugins/home/server/tutorials/coredns_metrics/index.ts index ad0ce4a58c738..19db58e3456e7 100644 --- a/src/plugins/home/server/tutorials/coredns_metrics/index.ts +++ b/src/plugins/home/server/tutorials/coredns_metrics/index.ts @@ -48,7 +48,7 @@ export function corednsMetricsSpecProvider(context: TutorialContext): TutorialSc learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-coredns.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/coredns.svg', + euiIconType: '/plugins/home/assets/logos/coredns.svg', artifacts: { application: { label: i18n.translate('home.tutorials.corednsMetrics.artifacts.application.label', { @@ -62,7 +62,7 @@ export function corednsMetricsSpecProvider(context: TutorialContext): TutorialSc }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/coredns_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/coredns_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts index e1423e96b1d47..1fbaa44817226 100644 --- a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts @@ -48,7 +48,7 @@ export function couchdbMetricsSpecProvider(context: TutorialContext): TutorialSc learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-couchdb.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/couchdb.svg', + euiIconType: '/plugins/home/assets/logos/couchdb.svg', artifacts: { dashboards: [ { @@ -67,7 +67,7 @@ export function couchdbMetricsSpecProvider(context: TutorialContext): TutorialSc }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/couchdb_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/couchdb_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/docker_metrics/index.ts b/src/plugins/home/server/tutorials/docker_metrics/index.ts index 4d9d0c9ee68d7..8c603697c4713 100644 --- a/src/plugins/home/server/tutorials/docker_metrics/index.ts +++ b/src/plugins/home/server/tutorials/docker_metrics/index.ts @@ -64,7 +64,7 @@ export function dockerMetricsSpecProvider(context: TutorialContext): TutorialSch }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/docker_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/docker_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts index 53803a9358a14..3d88cce36d752 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts @@ -50,7 +50,7 @@ It supports both standalone deployment and Envoy proxy deployment in Kubernetes. learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-envoyproxy.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/envoyproxy.svg', + euiIconType: '/plugins/home/assets/logos/envoyproxy.svg', artifacts: { dashboards: [], application: { @@ -64,7 +64,7 @@ It supports both standalone deployment and Envoy proxy deployment in Kubernetes. }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/envoyproxy_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/envoyproxy_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts index d405e77918546..adc7a494200c1 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts @@ -48,7 +48,7 @@ export function envoyproxyMetricsSpecProvider(context: TutorialContext): Tutoria learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-envoyproxy.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/envoyproxy.svg', + euiIconType: '/plugins/home/assets/logos/envoyproxy.svg', artifacts: { dashboards: [], exportedFields: { @@ -56,7 +56,6 @@ export function envoyproxyMetricsSpecProvider(context: TutorialContext): Tutoria }, }, completionTimeMinutes: 10, - // previewImagePath: '/plugins/kibana/home/tutorial_resources/envoyproxy_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts index 9922cb0e6341e..5739c03954def 100644 --- a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts +++ b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts @@ -48,7 +48,7 @@ export function ibmmqLogsSpecProvider(context: TutorialContext): TutorialSchema learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-ibmmq.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/ibmmq.svg', + euiIconType: '/plugins/home/assets/logos/ibmmq.svg', artifacts: { dashboards: [ { @@ -64,7 +64,7 @@ export function ibmmqLogsSpecProvider(context: TutorialContext): TutorialSchema }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/ibmmq_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/ibmmq_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts index 2055196f833b2..a6a1a9c6d3a06 100644 --- a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts @@ -48,7 +48,7 @@ export function ibmmqMetricsSpecProvider(context: TutorialContext): TutorialSche learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-ibmmq.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/ibmmq.svg', + euiIconType: '/plugins/home/assets/logos/ibmmq.svg', isBeta: true, artifacts: { application: { @@ -63,7 +63,7 @@ export function ibmmqMetricsSpecProvider(context: TutorialContext): TutorialSche }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/ibmmq_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/ibmmq_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/iis_logs/index.ts b/src/plugins/home/server/tutorials/iis_logs/index.ts index 82ce098018e0b..fee8d036db757 100644 --- a/src/plugins/home/server/tutorials/iis_logs/index.ts +++ b/src/plugins/home/server/tutorials/iis_logs/index.ts @@ -49,7 +49,7 @@ export function iisLogsSpecProvider(context: TutorialContext): TutorialSchema { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-iis.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/iis.svg', + euiIconType: '/plugins/home/assets/logos/iis.svg', artifacts: { dashboards: [ { @@ -65,7 +65,7 @@ export function iisLogsSpecProvider(context: TutorialContext): TutorialSchema { }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/iis_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/iis_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/iptables_logs/index.ts b/src/plugins/home/server/tutorials/iptables_logs/index.ts index b29ab20cb6653..e72e0ef300e04 100644 --- a/src/plugins/home/server/tutorials/iptables_logs/index.ts +++ b/src/plugins/home/server/tutorials/iptables_logs/index.ts @@ -52,7 +52,7 @@ number and the action performed on the traffic (allow/deny).. \ learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-iptables.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/ubiquiti.svg', + euiIconType: '/plugins/home/assets/logos/ubiquiti.svg', artifacts: { dashboards: [], application: { @@ -66,7 +66,7 @@ number and the action performed on the traffic (allow/deny).. \ }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/iptables_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/iptables_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/kafka_logs/index.ts b/src/plugins/home/server/tutorials/kafka_logs/index.ts index 74aa1ef772c85..746e65b71008c 100644 --- a/src/plugins/home/server/tutorials/kafka_logs/index.ts +++ b/src/plugins/home/server/tutorials/kafka_logs/index.ts @@ -65,7 +65,7 @@ export function kafkaLogsSpecProvider(context: TutorialContext): TutorialSchema }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/kafka_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/kafka_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts index 466f713d35e06..bcea7f1221e1f 100644 --- a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts +++ b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts @@ -67,7 +67,7 @@ export function kubernetesMetricsSpecProvider(context: TutorialContext): Tutoria }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/kubernetes_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/kubernetes_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/logstash_logs/index.ts b/src/plugins/home/server/tutorials/logstash_logs/index.ts index 276ceedbbcc68..69e498ac59459 100644 --- a/src/plugins/home/server/tutorials/logstash_logs/index.ts +++ b/src/plugins/home/server/tutorials/logstash_logs/index.ts @@ -65,7 +65,7 @@ export function logstashLogsSpecProvider(context: TutorialContext): TutorialSche }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/logstash_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/logstash_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts index 1a10dc3849471..f02695e207dd3 100644 --- a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts @@ -67,7 +67,7 @@ export function mongodbMetricsSpecProvider(context: TutorialContext): TutorialSc }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/mongodb_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/mongodb_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/mssql_metrics/index.ts b/src/plugins/home/server/tutorials/mssql_metrics/index.ts index a1c994d670a3d..4b418587f78b2 100644 --- a/src/plugins/home/server/tutorials/mssql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mssql_metrics/index.ts @@ -48,7 +48,7 @@ export function mssqlMetricsSpecProvider(context: TutorialContext): TutorialSche learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-mssql.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/mssql.svg', + euiIconType: '/plugins/home/assets/logos/mssql.svg', isBeta: false, artifacts: { dashboards: [ @@ -65,7 +65,7 @@ export function mssqlMetricsSpecProvider(context: TutorialContext): TutorialSche }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/mssql_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/mssql_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/munin_metrics/index.ts b/src/plugins/home/server/tutorials/munin_metrics/index.ts index 90e4ac6026dad..1d6b19c4cec2e 100644 --- a/src/plugins/home/server/tutorials/munin_metrics/index.ts +++ b/src/plugins/home/server/tutorials/munin_metrics/index.ts @@ -36,7 +36,7 @@ export function muninMetricsSpecProvider(context: TutorialContext): TutorialSche name: i18n.translate('home.tutorials.muninMetrics.nameTitle', { defaultMessage: 'Munin metrics', }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/munin.svg', + euiIconType: '/plugins/home/assets/logos/munin.svg', isBeta: true, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.muninMetrics.shortDescription', { diff --git a/src/plugins/home/server/tutorials/mysql_logs/index.ts b/src/plugins/home/server/tutorials/mysql_logs/index.ts index e003f4dfd47e4..178a371f9212e 100644 --- a/src/plugins/home/server/tutorials/mysql_logs/index.ts +++ b/src/plugins/home/server/tutorials/mysql_logs/index.ts @@ -65,7 +65,7 @@ export function mysqlLogsSpecProvider(context: TutorialContext): TutorialSchema }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/mysql_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/mysql_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/mysql_metrics/index.ts b/src/plugins/home/server/tutorials/mysql_metrics/index.ts index d18cc31512e71..1148caeb441f8 100644 --- a/src/plugins/home/server/tutorials/mysql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/mysql_metrics/index.ts @@ -64,7 +64,7 @@ export function mysqlMetricsSpecProvider(context: TutorialContext): TutorialSche }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/mysql_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/mysql_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/nats_logs/index.ts b/src/plugins/home/server/tutorials/nats_logs/index.ts index 3f6cb36d8d49e..17c37755b6bc3 100644 --- a/src/plugins/home/server/tutorials/nats_logs/index.ts +++ b/src/plugins/home/server/tutorials/nats_logs/index.ts @@ -50,7 +50,7 @@ export function natsLogsSpecProvider(context: TutorialContext): TutorialSchema { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-nats.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/nats.svg', + euiIconType: '/plugins/home/assets/logos/nats.svg', artifacts: { dashboards: [ { @@ -66,7 +66,7 @@ export function natsLogsSpecProvider(context: TutorialContext): TutorialSchema { }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/nats_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/nats_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/nats_metrics/index.ts b/src/plugins/home/server/tutorials/nats_metrics/index.ts index 27b5507ff6672..bce08e85c6977 100644 --- a/src/plugins/home/server/tutorials/nats_metrics/index.ts +++ b/src/plugins/home/server/tutorials/nats_metrics/index.ts @@ -48,7 +48,7 @@ export function natsMetricsSpecProvider(context: TutorialContext): TutorialSchem learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-nats.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/nats.svg', + euiIconType: '/plugins/home/assets/logos/nats.svg', artifacts: { dashboards: [ { @@ -64,7 +64,7 @@ export function natsMetricsSpecProvider(context: TutorialContext): TutorialSchem }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/nats_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/nats_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/nginx_logs/index.ts b/src/plugins/home/server/tutorials/nginx_logs/index.ts index 756d4a171d858..37d0cc106bfe5 100644 --- a/src/plugins/home/server/tutorials/nginx_logs/index.ts +++ b/src/plugins/home/server/tutorials/nginx_logs/index.ts @@ -65,7 +65,7 @@ export function nginxLogsSpecProvider(context: TutorialContext): TutorialSchema }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/nginx_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/nginx_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/nginx_metrics/index.ts b/src/plugins/home/server/tutorials/nginx_metrics/index.ts index 82af4d6c42dd8..8671f7218ffc8 100644 --- a/src/plugins/home/server/tutorials/nginx_metrics/index.ts +++ b/src/plugins/home/server/tutorials/nginx_metrics/index.ts @@ -69,7 +69,7 @@ which must be enabled in your Nginx installation. \ }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/nginx_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/nginx_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts index b0ff61c7116ce..eb539e15c1bcd 100644 --- a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts +++ b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts @@ -48,7 +48,7 @@ export function openmetricsMetricsSpecProvider(context: TutorialContext): Tutori learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-openmetrics.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/openmetrics.svg', + euiIconType: '/plugins/home/assets/logos/openmetrics.svg', artifacts: { dashboards: [], exportedFields: { diff --git a/src/plugins/home/server/tutorials/osquery_logs/index.ts b/src/plugins/home/server/tutorials/osquery_logs/index.ts index bce928519f66d..34a1b9e7f619d 100644 --- a/src/plugins/home/server/tutorials/osquery_logs/index.ts +++ b/src/plugins/home/server/tutorials/osquery_logs/index.ts @@ -65,7 +65,7 @@ export function osqueryLogsSpecProvider(context: TutorialContext): TutorialSchem }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/osquery_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/osquery_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts index a6c98fb16671f..975b549c9520b 100644 --- a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts +++ b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts @@ -63,7 +63,6 @@ export function phpfpmMetricsSpecProvider(context: TutorialContext): TutorialSch }, }, completionTimeMinutes: 10, - // previewImagePath: '/plugins/kibana/home/tutorial_resources/php_fpm_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/postgresql_logs/index.ts b/src/plugins/home/server/tutorials/postgresql_logs/index.ts index def9f71c9d2df..0c28061985819 100644 --- a/src/plugins/home/server/tutorials/postgresql_logs/index.ts +++ b/src/plugins/home/server/tutorials/postgresql_logs/index.ts @@ -68,7 +68,7 @@ export function postgresqlLogsSpecProvider(context: TutorialContext): TutorialSc }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/postgresql_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/postgresql_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts index b16267aeb0de6..f9bb9d249e755 100644 --- a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts +++ b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts @@ -65,7 +65,6 @@ export function postgresqlMetricsSpecProvider(context: TutorialContext): Tutoria }, }, completionTimeMinutes: 10, - // previewImagePath: '/plugins/kibana/home/tutorial_resources/postgresql_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts index b62a7ff731420..a646068e4ff34 100644 --- a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts +++ b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts @@ -68,7 +68,7 @@ export function rabbitmqMetricsSpecProvider(context: TutorialContext): TutorialS }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/rabbitmq_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/rabbitmq_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/redis_logs/index.ts b/src/plugins/home/server/tutorials/redis_logs/index.ts index 27c288ce9c381..e017fae0499a3 100644 --- a/src/plugins/home/server/tutorials/redis_logs/index.ts +++ b/src/plugins/home/server/tutorials/redis_logs/index.ts @@ -71,7 +71,7 @@ Note that the `slowlog` fileset is experimental. \ }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/redis_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/redis_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/redis_metrics/index.ts b/src/plugins/home/server/tutorials/redis_metrics/index.ts index 27c7780653168..bcc4d9bb0b67b 100644 --- a/src/plugins/home/server/tutorials/redis_metrics/index.ts +++ b/src/plugins/home/server/tutorials/redis_metrics/index.ts @@ -64,7 +64,7 @@ export function redisMetricsSpecProvider(context: TutorialContext): TutorialSche }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/redis_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/redis_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts index b352691f06afe..2c2246b15d7fa 100644 --- a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts +++ b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts @@ -63,8 +63,7 @@ export function redisenterpriseMetricsSpecProvider(context: TutorialContext): Tu }, }, completionTimeMinutes: 10, - previewImagePath: - '/plugins/kibana/home/tutorial_resources/redisenterprise_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/redisenterprise_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/stan_metrics/index.ts b/src/plugins/home/server/tutorials/stan_metrics/index.ts index 7dd949704d3cf..616bc7450249e 100644 --- a/src/plugins/home/server/tutorials/stan_metrics/index.ts +++ b/src/plugins/home/server/tutorials/stan_metrics/index.ts @@ -48,7 +48,7 @@ export function stanMetricsSpecProvider(context: TutorialContext): TutorialSchem learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-stan.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/stan.svg', + euiIconType: '/plugins/home/assets/logos/stan.svg', artifacts: { dashboards: [ { @@ -64,7 +64,7 @@ export function stanMetricsSpecProvider(context: TutorialContext): TutorialSchem }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/stan_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/stan_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/statsd_metrics/index.ts b/src/plugins/home/server/tutorials/statsd_metrics/index.ts index c1d4a354e9496..1dc297e78c791 100644 --- a/src/plugins/home/server/tutorials/statsd_metrics/index.ts +++ b/src/plugins/home/server/tutorials/statsd_metrics/index.ts @@ -45,7 +45,7 @@ export function statsdMetricsSpecProvider(context: TutorialContext): TutorialSch learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-statsd.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/statsd.svg', + euiIconType: '/plugins/home/assets/logos/statsd.svg', artifacts: { dashboards: [], exportedFields: { diff --git a/src/plugins/home/server/tutorials/suricata_logs/index.ts b/src/plugins/home/server/tutorials/suricata_logs/index.ts index a3812fda147f5..c02cb05889ebb 100644 --- a/src/plugins/home/server/tutorials/suricata_logs/index.ts +++ b/src/plugins/home/server/tutorials/suricata_logs/index.ts @@ -50,7 +50,7 @@ export function suricataLogsSpecProvider(context: TutorialContext): TutorialSche learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-suricata.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/suricata.svg', + euiIconType: '/plugins/home/assets/logos/suricata.svg', artifacts: { dashboards: [ { @@ -66,7 +66,7 @@ export function suricataLogsSpecProvider(context: TutorialContext): TutorialSche }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/suricata_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/suricata_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/system_logs/index.ts b/src/plugins/home/server/tutorials/system_logs/index.ts index ab8184c1b3249..9bad70699a6ed 100644 --- a/src/plugins/home/server/tutorials/system_logs/index.ts +++ b/src/plugins/home/server/tutorials/system_logs/index.ts @@ -50,7 +50,7 @@ Unix/Linux based distributions. This module is not available on Windows. \ learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-system.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/system.svg', + euiIconType: '/plugins/home/assets/logos/system.svg', artifacts: { dashboards: [ { @@ -66,7 +66,7 @@ Unix/Linux based distributions. This module is not available on Windows. \ }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/system_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/system_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/system_metrics/index.ts b/src/plugins/home/server/tutorials/system_metrics/index.ts index 456804c51f838..ef1a84ecdbf10 100644 --- a/src/plugins/home/server/tutorials/system_metrics/index.ts +++ b/src/plugins/home/server/tutorials/system_metrics/index.ts @@ -49,7 +49,7 @@ It collects system wide statistics and statistics per process and filesystem. \ learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-system.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/system.svg', + euiIconType: '/plugins/home/assets/logos/system.svg', artifacts: { dashboards: [ { @@ -65,7 +65,7 @@ It collects system wide statistics and statistics per process and filesystem. \ }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/system_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/system_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/traefik_logs/index.ts b/src/plugins/home/server/tutorials/traefik_logs/index.ts index 56f1d56ea0123..1876edd6c0bf7 100644 --- a/src/plugins/home/server/tutorials/traefik_logs/index.ts +++ b/src/plugins/home/server/tutorials/traefik_logs/index.ts @@ -49,7 +49,7 @@ export function traefikLogsSpecProvider(context: TutorialContext): TutorialSchem learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-traefik.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/traefik.svg', + euiIconType: '/plugins/home/assets/logos/traefik.svg', artifacts: { dashboards: [ { @@ -65,7 +65,7 @@ export function traefikLogsSpecProvider(context: TutorialContext): TutorialSchem }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/traefik_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/traefik_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/traefik_metrics/index.ts b/src/plugins/home/server/tutorials/traefik_metrics/index.ts index 8fe81eca4c601..a97ee3ab9758a 100644 --- a/src/plugins/home/server/tutorials/traefik_metrics/index.ts +++ b/src/plugins/home/server/tutorials/traefik_metrics/index.ts @@ -45,7 +45,7 @@ export function traefikMetricsSpecProvider(context: TutorialContext): TutorialSc learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-traefik.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/traefik.svg', + euiIconType: '/plugins/home/assets/logos/traefik.svg', artifacts: { dashboards: [], exportedFields: { @@ -53,7 +53,6 @@ export function traefikMetricsSpecProvider(context: TutorialContext): TutorialSc }, }, completionTimeMinutes: 10, - // previewImagePath: '/plugins/kibana/home/tutorial_resources/traefik_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/uptime_monitors/index.ts b/src/plugins/home/server/tutorials/uptime_monitors/index.ts index 207bc0cb479be..fa854a1c23505 100644 --- a/src/plugins/home/server/tutorials/uptime_monitors/index.ts +++ b/src/plugins/home/server/tutorials/uptime_monitors/index.ts @@ -62,7 +62,7 @@ export function uptimeMonitorsSpecProvider(context: TutorialContext): TutorialSc }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/uptime_monitors/screenshot.png', + previewImagePath: '/plugins/home/assets/uptime_monitors/screenshot.png', onPrem: onPremInstructions([], context), elasticCloud: cloudInstructions(), onPremElasticCloud: onPremCloudInstructions(), diff --git a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts index a1dfbc64ec244..bbe4ea78ee87c 100644 --- a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts +++ b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts @@ -48,7 +48,7 @@ export function uwsgiMetricsSpecProvider(context: TutorialContext): TutorialSche learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-uwsgi.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/uwsgi.svg', + euiIconType: '/plugins/home/assets/logos/uwsgi.svg', isBeta: false, artifacts: { dashboards: [ @@ -65,7 +65,7 @@ export function uwsgiMetricsSpecProvider(context: TutorialContext): TutorialSche }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/uwsgi_metrics/screenshot.png', + previewImagePath: '/plugins/home/assets/uwsgi_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts index 908b6440f88c6..81bf99f1ec3c1 100644 --- a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts +++ b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts @@ -48,7 +48,7 @@ export function vSphereMetricsSpecProvider(context: TutorialContext): TutorialSc learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-vsphere.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/vsphere.svg', + euiIconType: '/plugins/home/assets/logos/vsphere.svg', isBeta: true, artifacts: { application: { @@ -63,7 +63,6 @@ export function vSphereMetricsSpecProvider(context: TutorialContext): TutorialSc }, }, completionTimeMinutes: 10, - // previewImagePath: '/plugins/kibana/home/tutorial_resources/vsphere_metrics/screenshot.png', onPrem: onPremInstructions(moduleName, context), elasticCloud: cloudInstructions(moduleName), onPremElasticCloud: onPremCloudInstructions(moduleName), diff --git a/src/plugins/home/server/tutorials/zeek_logs/index.ts b/src/plugins/home/server/tutorials/zeek_logs/index.ts index 251825147ded1..4bd54c96481b6 100644 --- a/src/plugins/home/server/tutorials/zeek_logs/index.ts +++ b/src/plugins/home/server/tutorials/zeek_logs/index.ts @@ -50,7 +50,7 @@ export function zeekLogsSpecProvider(context: TutorialContext): TutorialSchema { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-zeek.html', }, }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/zeek.svg', + euiIconType: '/plugins/home/assets/logos/zeek.svg', artifacts: { dashboards: [ { @@ -66,7 +66,7 @@ export function zeekLogsSpecProvider(context: TutorialContext): TutorialSchema { }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/kibana/home/tutorial_resources/zeek_logs/screenshot.png', + previewImagePath: '/plugins/home/assets/zeek_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts index 581b4a14a2f38..f74f65cbc6b7d 100644 --- a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts +++ b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts @@ -36,7 +36,7 @@ export function zookeeperMetricsSpecProvider(context: TutorialContext): Tutorial name: i18n.translate('home.tutorials.zookeeperMetrics.nameTitle', { defaultMessage: 'Zookeeper metrics', }), - euiIconType: '/plugins/kibana/home/tutorial_resources/logos/zookeeper.svg', + euiIconType: '/plugins/home/assets/logos/zookeeper.svg', isBeta: false, category: TutorialsCategory.METRICS, shortDescription: i18n.translate('home.tutorials.zookeeperMetrics.shortDescription', { diff --git a/x-pack/legacy/plugins/maps/server/tutorials/ems/index.ts b/x-pack/legacy/plugins/maps/server/tutorials/ems/index.ts index 88c22d01a527a..1006d36afa34d 100644 --- a/x-pack/legacy/plugins/maps/server/tutorials/ems/index.ts +++ b/x-pack/legacy/plugins/maps/server/tutorials/ems/index.ts @@ -31,7 +31,7 @@ Indexing EMS administrative boundaries in Elasticsearch allows for search on bou }), euiIconType: 'emsApp', completionTimeMinutes: 1, - previewImagePath: '/plugins/kibana/home/tutorial_resources/ems/boundaries_screenshot.png', + previewImagePath: '/plugins/maps/assets/boundaries_screenshot.png', onPrem: { instructionSets: [ { diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/apm/apm.png b/x-pack/plugins/apm/public/assets/apm.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/apm/apm.png rename to x-pack/plugins/apm/public/assets/apm.png diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index 1fbac0b6495df..d8cbff9a1c27d 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -98,7 +98,7 @@ It allows you to monitor the performance of thousands of applications in real ti artifacts, onPrem: onPremInstructions(indices), elasticCloud: createElasticCloudInstructions(cloud), - previewImagePath: '/plugins/kibana/home/tutorial_resources/apm/apm.png', + previewImagePath: '/plugins/apm/assets/apm.png', savedObjects, savedObjectsInstallMsg: i18n.translate( 'xpack.apm.tutorial.specProvider.savedObjectsInstallMsg', diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/ems/boundaries_screenshot.png b/x-pack/plugins/maps/public/assets/boundaries_screenshot.png similarity index 100% rename from src/legacy/core_plugins/kibana/public/home/tutorial_resources/ems/boundaries_screenshot.png rename to x-pack/plugins/maps/public/assets/boundaries_screenshot.png From 5754912ae4a6fb4cbb1c810d8dba688219d070ce Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Mon, 4 May 2020 14:45:39 +0300 Subject: [PATCH 077/122] Adjust kibana app owning files (#65064) --- .github/CODEOWNERS | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3981a8e1e9afe..b4692a4ddb3b7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,18 +11,21 @@ /src/legacy/core_plugins/kibana/public/discover/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app -/src/plugins/vis_type_vislib/ @elastic/kibana-app -/src/plugins/vis_type_xy/ @elastic/kibana-app -/src/plugins/vis_type_table/ @elastic/kibana-app -/src/plugins/kibana_legacy/ @elastic/kibana-app -/src/plugins/vis_type_timelion/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app /src/plugins/input_control_vis/ @elastic/kibana-app -/src/plugins/visualize/ @elastic/kibana-app -/src/plugins/vis_type_timeseries/ @elastic/kibana-app -/src/plugins/vis_type_metric/ @elastic/kibana-app +/src/plugins/kibana_legacy/ @elastic/kibana-app +/src/plugins/vis_default_editor/ @elastic/kibana-app /src/plugins/vis_type_markdown/ @elastic/kibana-app +/src/plugins/vis_type_metric/ @elastic/kibana-app +/src/plugins/vis_type_table/ @elastic/kibana-app +/src/plugins/vis_type_tagcloud/ @elastic/kibana-app +/src/plugins/vis_type_timelion/ @elastic/kibana-app +/src/plugins/vis_type_timeseries/ @elastic/kibana-app +/src/plugins/vis_type_vega/ @elastic/kibana-app +/src/plugins/vis_type_vislib/ @elastic/kibana-app +/src/plugins/vis_type_xy/ @elastic/kibana-app +/src/plugins/visualize/ @elastic/kibana-app # Core UI # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon From 5bcf2c8b892d0b456b0dcde18cafc3a401013e9e Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Mon, 4 May 2020 13:03:54 +0100 Subject: [PATCH 078/122] [Logs UI] [Alerting] Alerts management page enhancements (#64654) * Ensure adding / editing log alerts works from the alerts management page --- .../components/alerting/logs/alert_flyout.tsx | 4 +- .../logs/expression_editor/editor.tsx | 108 ++++++++++++++---- .../api/fetch_log_source_configuration.ts | 9 +- .../log_source/api/fetch_log_source_status.ts | 6 +- .../api/patch_log_source_configuration.ts | 7 +- .../containers/logs/log_source/log_source.ts | 26 +++-- .../public/pages/logs/page_providers.tsx | 6 +- 7 files changed, 126 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx b/x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx index b18c2e5b8d69c..37cea9314cfe8 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx @@ -24,7 +24,9 @@ export const AlertFlyout = (props: Props) => { {triggersActionsUI && ( ; setAlertParams(key: string, value: any): void; setAlertProperty(key: string, value: any): void; + alertsContext: AlertsContextValue; } const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' }; @@ -48,32 +58,92 @@ const DEFAULT_EXPRESSION = { }; export const ExpressionEditor: React.FC = props => { + const isInternal = props.alertsContext.metadata?.isInternal; + const [sourceId] = useSourceId(); + + return ( + <> + {isInternal ? ( + + + + ) : ( + + + + + + )} + + ); +}; + +export const SourceStatusWrapper: React.FC = props => { + const { + initialize, + isLoadingSourceStatus, + isUninitialized, + hasFailedLoadingSourceStatus, + loadSourceStatus, + } = useLogSourceContext(); + const { children } = props; + + useMount(() => { + initialize(); + }); + + return ( + <> + {isLoadingSourceStatus || isUninitialized ? ( +
    + + + +
    + ) : hasFailedLoadingSourceStatus ? ( + + + {i18n.translate('xpack.infra.logs.alertFlyout.sourceStatusErrorTryAgain', { + defaultMessage: 'Try again', + })} + + + ) : ( + children + )} + + ); +}; + +export const Editor: React.FC = props => { const { setAlertParams, alertParams, errors } = props; - const { createDerivedIndexPattern } = useSource({ sourceId: 'default' }); const [timeSize, setTimeSize] = useState(1); const [timeUnit, setTimeUnit] = useState('m'); const [hasSetDefaults, setHasSetDefaults] = useState(false); - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('logs'), [ - createDerivedIndexPattern, - ]); + const { sourceStatus } = useLogSourceContext(); + + useMount(() => { + for (const [key, value] of Object.entries(DEFAULT_EXPRESSION)) { + setAlertParams(key, value); + setHasSetDefaults(true); + } + }); const supportedFields = useMemo(() => { - if (derivedIndexPattern?.fields) { - return derivedIndexPattern.fields.filter(field => { + if (sourceStatus?.logIndexFields) { + return sourceStatus.logIndexFields.filter(field => { return (field.type === 'string' || field.type === 'number') && field.searchable; }); } else { return []; } - }, [derivedIndexPattern]); - - // Set the default expression (disables exhaustive-deps as we only want to run this once on mount) - useEffect(() => { - for (const [key, value] of Object.entries(DEFAULT_EXPRESSION)) { - setAlertParams(key, value); - setHasSetDefaults(true); - } - }, []); // eslint-disable-line react-hooks/exhaustive-deps + }, [sourceStatus]); const updateCount = useCallback( countParams => { @@ -126,8 +196,6 @@ export const ExpressionEditor: React.FC = props => { [alertParams, setAlertParams] ); - // Wait until field info has loaded - if (supportedFields.length === 0) return null; // Wait until the alert param defaults have been set if (!hasSetDefaults) return null; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts index 786cb485b38dd..e847302a6d367 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpSetup } from 'src/core/public'; import { getLogSourceConfigurationPath, getLogSourceConfigurationSuccessResponsePayloadRT, } from '../../../../../common/http_api/log_sources'; import { decodeOrThrow } from '../../../../../common/runtime_types'; -import { npStart } from '../../../../legacy_singletons'; -export const callFetchLogSourceConfigurationAPI = async (sourceId: string) => { - const response = await npStart.http.fetch(getLogSourceConfigurationPath(sourceId), { +export const callFetchLogSourceConfigurationAPI = async ( + sourceId: string, + fetch: HttpSetup['fetch'] +) => { + const response = await fetch(getLogSourceConfigurationPath(sourceId), { method: 'GET', }); diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts index 2f1d15ffaf4d3..20e67a0a59c9f 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpSetup } from 'src/core/public'; import { getLogSourceStatusPath, getLogSourceStatusSuccessResponsePayloadRT, } from '../../../../../common/http_api/log_sources'; import { decodeOrThrow } from '../../../../../common/runtime_types'; -import { npStart } from '../../../../legacy_singletons'; -export const callFetchLogSourceStatusAPI = async (sourceId: string) => { - const response = await npStart.http.fetch(getLogSourceStatusPath(sourceId), { +export const callFetchLogSourceStatusAPI = async (sourceId: string, fetch: HttpSetup['fetch']) => { + const response = await fetch(getLogSourceStatusPath(sourceId), { method: 'GET', }); diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts index 848801ab3c7ce..4361e4bef827f 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpSetup } from 'src/core/public'; import { getLogSourceConfigurationPath, patchLogSourceConfigurationSuccessResponsePayloadRT, @@ -11,13 +12,13 @@ import { LogSourceConfigurationPropertiesPatch, } from '../../../../../common/http_api/log_sources'; import { decodeOrThrow } from '../../../../../common/runtime_types'; -import { npStart } from '../../../../legacy_singletons'; export const callPatchLogSourceConfigurationAPI = async ( sourceId: string, - patchedProperties: LogSourceConfigurationPropertiesPatch + patchedProperties: LogSourceConfigurationPropertiesPatch, + fetch: HttpSetup['fetch'] ) => { - const response = await npStart.http.fetch(getLogSourceConfigurationPath(sourceId), { + const response = await fetch(getLogSourceConfigurationPath(sourceId), { method: 'PATCH', body: JSON.stringify( patchLogSourceConfigurationRequestBodyRT.encode({ diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts index 8332018fddf90..670988d680147 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts @@ -6,6 +6,7 @@ import createContainer from 'constate'; import { useState, useMemo, useCallback } from 'react'; +import { HttpSetup } from 'src/core/public'; import { LogSourceConfiguration, LogSourceStatus, @@ -24,7 +25,13 @@ export { LogSourceStatus, }; -export const useLogSource = ({ sourceId }: { sourceId: string }) => { +export const useLogSource = ({ + sourceId, + fetch, +}: { + sourceId: string; + fetch: HttpSetup['fetch']; +}) => { const [sourceConfiguration, setSourceConfiguration] = useState< LogSourceConfiguration | undefined >(undefined); @@ -35,40 +42,40 @@ export const useLogSource = ({ sourceId }: { sourceId: string }) => { { cancelPreviousOn: 'resolution', createPromise: async () => { - return await callFetchLogSourceConfigurationAPI(sourceId); + return await callFetchLogSourceConfigurationAPI(sourceId, fetch); }, onResolve: ({ data }) => { setSourceConfiguration(data); }, }, - [sourceId] + [sourceId, fetch] ); const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise( { cancelPreviousOn: 'resolution', createPromise: async (patchedProperties: LogSourceConfigurationPropertiesPatch) => { - return await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties); + return await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties, fetch); }, onResolve: ({ data }) => { setSourceConfiguration(data); loadSourceStatus(); }, }, - [sourceId] + [sourceId, fetch] ); const [loadSourceStatusRequest, loadSourceStatus] = useTrackedPromise( { cancelPreviousOn: 'resolution', createPromise: async () => { - return await callFetchLogSourceStatusAPI(sourceId); + return await callFetchLogSourceStatusAPI(sourceId, fetch); }, onResolve: ({ data }) => { setSourceStatus(data); }, }, - [sourceId] + [sourceId, fetch] ); const logIndicesExist = useMemo(() => (sourceStatus?.logIndexNames?.length ?? 0) > 0, [ @@ -114,6 +121,10 @@ export const useLogSource = ({ sourceId }: { sourceId: string }) => { [loadSourceConfigurationRequest.state] ); + const hasFailedLoadingSourceStatus = useMemo(() => loadSourceStatusRequest.state === 'rejected', [ + loadSourceStatusRequest.state, + ]); + const loadSourceFailureMessage = useMemo( () => loadSourceConfigurationRequest.state === 'rejected' @@ -137,6 +148,7 @@ export const useLogSource = ({ sourceId }: { sourceId: string }) => { return { derivedIndexPattern, hasFailedLoadingSource, + hasFailedLoadingSourceStatus, initialize, isLoading, isLoadingSourceConfiguration, diff --git a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx index d2db5002f4aa2..1e053d8d4abc3 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx @@ -5,16 +5,16 @@ */ import React from 'react'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { LogAnalysisCapabilitiesProvider } from '../../containers/logs/log_analysis'; import { LogSourceProvider } from '../../containers/logs/log_source'; -// import { SourceProvider } from '../../containers/source'; import { useSourceId } from '../../containers/source_id'; export const LogsPageProviders: React.FunctionComponent = ({ children }) => { const [sourceId] = useSourceId(); - + const { services } = useKibana(); return ( - + {children} ); From 3356a19294e22fcff13c67a86cac64fab5dc3e7e Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Mon, 4 May 2020 08:33:02 -0400 Subject: [PATCH 079/122] update endpoint to restrict removing with datasources (#64978) --- .../server/services/epm/packages/remove.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 0d9db1697d886..23e63f0a89a5e 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -6,11 +6,12 @@ import { SavedObjectsClientContract } from 'src/core/server'; import Boom from 'boom'; -import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; +import { PACKAGES_SAVED_OBJECT_TYPE, DATASOURCE_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, AssetType, ElasticsearchAssetType } from '../../../types'; import { CallESAsCurrentUser } from '../../../types'; import { getInstallation, savedObjectTypes } from './index'; import { installIndexPatterns } from '../kibana/index_pattern/install'; +import { datasourceService } from '../..'; export async function removeInstallation(options: { savedObjectsClient: SavedObjectsClientContract; @@ -26,6 +27,17 @@ export async function removeInstallation(options: { throw Boom.badRequest(`${pkgName} is installed by default and cannot be removed`); const installedObjects = installation.installed || []; + const { total } = await datasourceService.list(savedObjectsClient, { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name:${pkgName}`, + page: 0, + perPage: 0, + }); + + if (total > 0) + throw Boom.badRequest( + `unable to remove package with existing datasource(s) in use by agent(s)` + ); + // Delete the manager saved object with references to the asset objects // could also update with [] or some other state await savedObjectsClient.delete(PACKAGES_SAVED_OBJECT_TYPE, pkgName); From 9cfe4cf6595dd13936cf5dac7bde33074e973d4e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 4 May 2020 13:39:25 +0100 Subject: [PATCH 080/122] [Event Log] Ensure sorting tests are less flaky (#64781) Creating events in parallel may be causing a slight flakyness, this change staggers creation to ensure this doesn't happen. In addition it turned out the `event.end` field was missing in certain cases, causing the test that sorts by `end` to fail. --- .../plugins/event_log/server/init_routes.ts | 3 ++ .../event_log/public_api_integration.ts | 33 ++++++++++--------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts index 9622715e87e55..d7fe8dd0dedf3 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts @@ -42,6 +42,9 @@ export const logEventRoute = (router: IRouter, eventLogger: IEventLogger, logger await context.core.savedObjects.client.create('event_log_test', {}, { id }); logger.info(`created saved object ${id}`); } + // mark now as start and end + eventLogger.startTiming(event); + eventLogger.stopTiming(event); eventLogger.logEvent(event); logger.info(`logged`); return res.ok({}); diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts index f3a3d58336b1d..a2ae165986340 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -19,9 +19,7 @@ export default function({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/64723 - // FLAKY: https://github.com/elastic/kibana/issues/64812 - describe.skip('Event Log public API', () => { + describe('Event Log public API', () => { it('should allow querying for events by Saved Object', async () => { const id = uuid.v4(); @@ -82,21 +80,15 @@ export default function({ getService }: FtrProviderContext) { it('should support sorting by event end', async () => { const id = uuid.v4(); - const [firstExpectedEvent, ...expectedEvents] = times(6, () => fakeEvent(id)); - // run one first to create the SO and avoid clashes - await logTestEvent(id, firstExpectedEvent); - await Promise.all(expectedEvents.map(event => logTestEvent(id, event))); + const expectedEvents = await logFakeEvents(id, 6); await retry.try(async () => { const { body: { data: foundEvents }, } = await findEvents(id, { sort_field: 'event.end', sort_order: 'desc' }); - expect(foundEvents.length).to.be(6); - assertEventsFromApiMatchCreatedEvents( - foundEvents, - [firstExpectedEvent, ...expectedEvents].reverse() - ); + expect(foundEvents.length).to.be(expectedEvents.length); + assertEventsFromApiMatchCreatedEvents(foundEvents, expectedEvents.reverse()); }); }); @@ -112,8 +104,7 @@ export default function({ getService }: FtrProviderContext) { const start = new Date().toISOString(); // write the documents that we should be found in the date range searches - const expectedEvents = times(6, () => fakeEvent(id)); - await Promise.all(expectedEvents.map(event => logTestEvent(id, event))); + const expectedEvents = await logFakeEvents(id, 6); // get the end time for the date range search const end = new Date().toISOString(); @@ -176,7 +167,9 @@ export default function({ getService }: FtrProviderContext) { ) { try { foundEvents.forEach((foundEvent: IValidatedEvent, index: number) => { - expect(foundEvent!.event).to.eql(expectedEvents[index]!.event); + expect(omit(foundEvent!.event ?? {}, 'start', 'end', 'duration')).to.eql( + expectedEvents[index]!.event + ); expect(omit(foundEvent!.kibana ?? {}, 'server_uuid')).to.eql(expectedEvents[index]!.kibana); expect(foundEvent!.message).to.eql(expectedEvents[index]!.message); }); @@ -217,4 +210,14 @@ export default function({ getService }: FtrProviderContext) { overrides ); } + + async function logFakeEvents(savedObjectId: string, eventsToLog: number): Promise { + const expectedEvents: IEvent[] = []; + for (let index = 0; index < eventsToLog; index++) { + const event = fakeEvent(savedObjectId); + await logTestEvent(savedObjectId, event); + expectedEvents.push(event); + } + return expectedEvents; + } } From cb00e5e7bb76611c628f850539fc70060e95360e Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Mon, 4 May 2020 08:48:57 -0400 Subject: [PATCH 081/122] [Alerting] fix labels and links in PagerDuty action ui and docs (#64032) resolves #63222, resolves #63768, resolves #63223 ui changes: - adds an "(optional)" label after the API URL label - changes help link to go to alerting docs and not watcher docs - changes the label "Routing key" to "Integration key" to match other docs - changes the order of the severity options to match other docs doc changes: - changes the reference of "Routing key" to "Integration key" to match other docs - makes clearer that the API URL is optional --- .../alerting/action-types/pagerduty.asciidoc | 4 ++-- .../builtin_action_types/pagerduty.tsx | 24 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index abdcc7d1ba524..673b4f6263e18 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -92,7 +92,7 @@ section of the alert configuration and selecting *Add new*. * Alternatively, create a connector by navigating to *Management* from the {kib} navbar and selecting *Alerts and Actions*. Then, select the *Connectors* tab, click the *Create connector* button, and select the PagerDuty option. -. Configure the connector by giving it a name and optionally entering the API URL and Routing Key, or using the defaults. +. Configure the connector by giving it a name and entering the Integration Key, optionally entering a custom API URL. + See <> for how to obtain the endpoint and key information from PagerDuty and <> for more details. @@ -133,7 +133,7 @@ PagerDuty connectors have the following configuration properties: Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. API URL:: An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <> setting, make sure the hostname is whitelisted. -Routing Key:: A 32 character PagerDuty Integration Key for an integration on a service or on a global ruleset. +Integration Key:: A 32 character PagerDuty Integration Key for an integration on a service, also referred to as the routing key. [float] [[pagerduty-action-configuration]] diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx index 15f91ae1d4609..e1c30ee1e8146 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx @@ -115,7 +115,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent @@ -139,7 +139,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent @@ -196,20 +196,20 @@ const PagerDutyParamsFields: React.FunctionComponent Date: Mon, 4 May 2020 16:11:20 +0200 Subject: [PATCH 082/122] Drilldowns (#61219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add drilldown wizard components * Dynamic actions (#58216) * feat: 🎸 add DynamicAction and FactoryAction types * feat: 🎸 add Mutable type to @kbn/utility-types * feat: 🎸 add ActionInternal and ActionContract * chore: 🤖 remove unused file * feat: 🎸 improve action interfaces * docs: ✏️ add JSDocs * feat: 🎸 simplify ui_actions interfaces * fix: 🐛 fix TypeScript types * feat: 🎸 add AbstractPresentable interface * feat: 🎸 add AbstractConfigurable interface * feat: 🎸 use AbstractPresentable in ActionInternal * test: 💍 fix ui_actions Jest tests * feat: 🎸 add state container to action * perf: ⚡️ convert MenuItem to React component on Action instance * refactor: 💡 rename AbsractPresentable -> Presentable * refactor: 💡 rename AbstractConfigurable -> Configurable * feat: 🎸 add Storybook to ui_actions * feat: 🎸 add component * feat: 🎸 improve component * chore: 🤖 use .story file extension prefix for Storybook * feat: 🎸 improve component * feat: 🎸 show error if dynamic action has CollectConfig missing * feat: 🎸 render sample action configuration component * feat: 🎸 connect action config to * feat: 🎸 improve stories * test: 💍 add ActionInternal serialize/deserialize tests * feat: 🎸 add ActionContract * feat: 🎸 split action Context into Execution and Presentation * fix: 🐛 fix TypeScript error * refactor: 💡 extract state container hooks to module scope * docs: ✏️ fix typos * chore: 🤖 remove Mutable type * test: 💍 don't cast to any getActions() function * style: 💄 avoid using unnecessary types * chore: 🤖 address PR review comments * chore: 🤖 rename ActionContext generic * chore: 🤖 remove order from state container * chore: 🤖 remove deprecation notice on getHref * test: 💍 fix tests after order field change * remove comments Co-authored-by: Matt Kime Co-authored-by: Elastic Machine * Drilldown context menu (#59638) * fix: 🐛 fix TypeScript error * feat: 🎸 add CONTEXT_MENU_DRILLDOWNS_TRIGGER trigger * fix: 🐛 correctly order context menu items * fix: 🐛 set correct order on drilldown flyout actions * fix: 🐛 clean up context menu building functions * feat: 🎸 add context menu separator action * Add basic ActionFactoryService. Pass data from it into components instead of mocks * Dashboard x pack (#59653) * feat: 🎸 add dashboard_enhanced plugin to x-pack * feat: 🎸 improve context menu separator * feat: 🎸 move drilldown flyout actions to dashboard_enhanced * fix: 🐛 fix exports from ui_actions plugin * feat: 🎸 "implement" registerDrilldown() method * fix ConfigurableBaseConfig type * Implement connected flyout_manage_drilldowns component * Simplify connected flyout manage drilldowns component. Remove intermediate component * clean up data-testid workaround in new components * Connect welcome message to storage Not sure, but use LocalStorage. Didn’t find a way to persist user settings. looks like uiSettings are not user scoped. * require `context` in Presentable. drill context down through wizard components * Drilldown factory (#59823) * refactor: 💡 import storage interface from ui_actions plugin * refactor: 💡 make actions not-dynamic * feat: 🎸 fix TypeScript errors, reshuffle types and code * fix: 🐛 fix more TypeScript errors * fix: 🐛 fix TypeScript import error * Drilldown registration (#59834) * feat: 🎸 improve drilldown registration method * fix: 🐛 set up translations for dashboard_enhanced plugin * Drilldown events 3 (#59854) * feat: 🎸 add serialize/unserialize to action * feat: 🎸 pass in uiActions service into Embeddable * feat: 🎸 merge ui_actions oss and basic plugins * refactor: 💡 move action factory registry to OSS * fix: 🐛 fix TypeScript errors * Drilldown events 4 (#59876) * feat: 🎸 mock sample drilldown execute methods * feat: 🎸 add .dynamicActions manager to Embeddable * feat: 🎸 add first version of dynamic action manager * Drilldown events 5 (#59885) * feat: 🎸 display drilldowns in context menu only on one embed * feat: 🎸 clear dynamic actions from registry when embed unloads * fix: 🐛 fix OSS TypeScript errors * basic integration of components with dynamicActionManager * fix: 🐛 don't overwrite explicitInput with combined input (#59938) * display drilldown count in embeddable edit mode * display drilldown count in embeddable edit mode * improve wizard components. more tests. * partial progress, dashboard drilldowns (#59977) * partial progress, dashboard drilldowns * partial progress, dashboard drilldowns * feat: 🎸 improve dashboard drilldown setup * feat: 🎸 wire in services into dashboard drilldown * chore: 🤖 add Storybook to dashboard_enhanced * feat: 🎸 create presentational * test: 💍 add stories * test: 💍 use presentation dashboar config component * feat: 🎸 wire in services into React component * docs: ✏️ add README to /components folder * feat: 🎸 increase importance of Dashboard drilldown * feat: 🎸 improve icon definition in drilldowns * chore: 🤖 remove unnecessary comment * chore: 🤖 add todos Co-authored-by: streamich * Manage drilldowns toasts. Add basic error handling. * support order in action factory selector * fix column order in manage drilldowns list * remove accidental debug info * bunch of nit ui fixes * Drilldowns reactive action manager (#60099) * feat: 🎸 improve isConfigValid return type * feat: 🎸 make DynamicActionManager reactive * docs: ✏️ add JSDocs to public mehtods of DynamicActionManager * feat: 🎸 make panel top-right corner number badge reactive * fix: 🐛 correctly await for .deleteEvents() * Drilldowns various 2 (#60103) * chore: 🤖 address review comments * test: 💍 fix embeddable_panel.test.tsx tests * chore: 🤖 clean up ActionInternal * chore: 🤖 make isConfigValid a simple predicate * chore: 🤖 fix TypeScript type errors * test: 💍 stub DynamicActionManager tests (#60104) * Drilldowns review 1 (#60139) * refactor: 💡 improve generic types * fix: 🐛 don't overwrite icon * fix: 🐛 fix x-pack TypeScript errors * fix: 🐛 fix TypeScript error * fix: 🐛 correct merge * Drilldowns various 4 (#60264) * feat: 🎸 hide "Create drilldown" from context menu when needed * style: 💄 remove AnyDrilldown type * feat: 🎸 add drilldown factory context * chore: 🤖 remove sample drilldown * fix: 🐛 increase spacing between action factory picker * workaround issue with closing flyout when navigating away Adds overlay just like other flyouts which makes this defect harder to bump in * fix react key issue in action_wizard * don’t open 2 flyouts * fix action order https://github.com/elastic/kibana/issues/60138 * Drilldowns reload stored (#60336) * style: 💄 don't use double equals __ * feat: 🎸 add reload$ to ActionStorage interface * feat: 🎸 add reload$ to embeddable event storage * feat: 🎸 add storage syncing to DynamicActionManager * refactor: 💡 use state from DynamicActionManager in React * fix: 🐛 add check for manager being stopped * Drilldowns triggers (#60339) * feat: 🎸 make use of supportedTriggers() * feat: 🎸 pass in context to configuration component * feat: 🎸 augment factory context * fix: 🐛 stop infinite re-rendering * Drilldowns multitrigger (#60357) * feat: 🎸 add support for multiple triggers * feat: 🎸 enable Drilldowns for TSVB Although TSVB brushing event is now broken on master, KibanaApp plans to fix it in 7.7 * "Create drilldown" flyout - design cleanup (#60309) * create drilldown flyout cleanup * remove border from selectedActionFactoryContainer * adjust callout in DrilldownHello * update form labels * remove unused file * fix type error Co-authored-by: Anton Dosov * basic unit tests for flyout_create_drildown action * Drilldowns finalize (#60371) * fix: 🐛 align flyout content to left side * fix: 🐛 move context menu item number 1px lower * fix: 🐛 move flyout back nav chevron up * fix: 🐛 fix type check after refactor * basic unit tests for drilldown actions * Drilldowns finalize 2 (#60510) * test: 💍 fix test mock * chore: 🤖 remove unused UiActionsService methods * refactor: 💡 cleanup UiActionsService action registration * fix: 🐛 add missing functionality after refactor * test: 💍 add action factory tests * test: 💍 add DynamicActionManager tests * feat: 🎸 capture error if it happens during initial load * fix: 🐛 register correctly CSV action * feat: 🎸 don't show "OPTIONS" title on drilldown context menus * feat: 🎸 add server-side for x-pack dashboard plugin * feat: 🎸 disable Drilldowns for TSVB * feat: 🎸 enable drilldowns on kibana.yml feature flag * feat: 🎸 add feature flag comment to kibana.yml * feat: 🎸 remove places from drilldown interface * refactor: 💡 remove place in factory context * chore: 🤖 remove doExecute * remove not needed now error_configure_action component * remove workaround for storybook * feat: 🎸 improve DrilldownDefinition interface * style: 💄 replace any by unknown * chore: 🤖 remove any * chore: 🤖 make isConfigValid return type a boolean * refactor: 💡 move getDisplayName to factory, remove deprecated * style: 💄 remove any * feat: 🎸 improve ActionFactoryDefinition * refactor: 💡 change visualize_embeddable params * feat: 🎸 add dashboard dependency to dashboard_enhanced * style: 💄 rename drilldown plugin life-cycle contracts * refactor: 💡 do naming adjustments for dashboard drilldown * fix: 🐛 fix Type error * fix: 🐛 fix TypeScript type errors * test: 💍 fix test after refactor * refactor: 💡 rename context -> placeContext in React component * chore: 🤖 remove setting from kibana.yml * refactor: 💡 change return type of getAction as per review * remove custom css per review * refactor: 💡 rename drilldownCount to eventCount * style: 💄 remove any * refactor: 💡 change how uiActions are passed to vis embeddable * style: 💄 remove unused import * fix: 🐛 pass in uiActions to visualize_embeddable * fix: 🐛 correctly register action * fix: 🐛 fix type error * chore: 🤖 remove unused translations * Dynamic actions to xpack (#62647) * feat: 🎸 set up sample action factory provider * feat: 🎸 create dashboard_enhanced plugin * feat: 🎸 add EnhancedEmbeddable interface * refactor: 💡 move DynamicActionManager to x-pack * feat: 🎸 connect dynamic action manager to embeddable life-cycle * test: 💍 fix Jest tests after refactor * fix: 🐛 fix type error Co-authored-by: Elastic Machine * refactor: 💡 move action factories to x-pack (#63190) * refactor: 💡 move action factories to x-pack * fix: 🐛 use correct plugin embeddable deps * test: 💍 fix Jest test after refactor * chore: 🤖 remove kibana.yml flag (#62441) * Panel top right (#63466) * feat: 🎸 add PANEL_NOTIFICATION_TRIGGER * feat: 🎸 add PanelNotificationsAction action * test: 💍 add PanelNotificationsAction unit tests * refactor: 💡 revert addTriggerAction() change * style: 💄 remove unused import * fix: 🐛 fix typecheck errors after merge * support getHref in drilldowns (#63727) * chore: 🤖 remove ui_actions storybook config * update docs * fix ts * fix: 🐛 fix broken merge * [Drilldowns] Dashboard to dashboard drilldown (#63108) * partial progress on async loading / searching of dashboard titles * feat: 🎸 make combobox full width * filtering combobox polish * storybook fix * implement navigating to dashboard, seems like a type problem * try navToApp * filter out current dashboard * rough draft linking to a dashboard * remove note * typefix * fix navigation from dashboard to dashboard except for back button - that would be addressed separatly * partial progress getting filters from action data * fix issue with getIndexPatterns undefined we can’t import those functions as static functions, instead we have to expose them on plugin contract because they are statefull * fix filter / time passing into url * typefix * dashboard to dashboard drilldown functional test and back button fix * documentation update * chore clean-ups fix type * basic unit test for dashboard drilldown * remove test todos decided to skip those tests because not clear how to test due to EuiCombobox is using react-virtualized and options list is not rendered in jsdom env * remove config * improve back button with filter comparison tweak * dashboard filters/date option off by default * revert change to config/kibana.yml * remove unneeded comments * use default time range as appropriate * fix type, add filter icon, add text * fix test * change how time range is restored and improve back button for drilldowns * resolve conflicts * fix async compile issue * remove redundant test * wip * wip * fix * temp skip tests * fix * handle missing dashboard edge case * fix api * refactor action filter creation utils * updating * updating docs * improve * fix storybook * post merge fixes * fix payload emitted in brush event * properly export createRange action * improve tests * add test * post merge fixes * improve * fix * improve * fix build * wip getHref support * implement getHref() * give proper name to a story * use sync start services * update text * fix types * fix ts * fix docs * move clone below drilldowns (near replace) * remove redundant comments * refactor action filter creation utils * updating * updating docs * fix payload emitted in brush event * properly export createRange action * some more updates * fixing types * ... * inline EventData * fix typescript in lens and update docs * improve filters types * docs * merge * @mdefazio review * adjust actions order * docs * @stacey-gammon review Co-authored-by: Matt Kime Co-authored-by: streamich Co-authored-by: ppisljar * fix docs * nit fixes * chore: 🤖 remove uiActions from Embeddable dependencies * chore: 🤖 don't export ActionInternal from ui_actions * test: 💍 remove uiActions deps in x-pack test mocks * chore: 🤖 cleanup ui_actions types * docs: ✏️ add JSDoc comment to addTriggerAction() * docs: ✏️ regenerate docs * Drilldown demo 2 (#64300) * chore: 🤖 add example of Discover drilldown to sample plugin * fix: 🐛 show drilldowns with higher "order" first * feat: 🎸 add createStartServicesGetter() to /public kibana_util * feat: 🎸 load index patterns in Discover drilldown * feat: 🎸 add toggle for index pattern selection * feat: 🎸 add spacer to separate unrelated config fields * fix: 🐛 correctly configre setup core * feat: 🎸 navigate to correct index pattern * chore: 🤖 fix type check errors * fix: 🐛 make index pattern select full width * fix: 🐛 add getHref support * feat: 🎸 add example plugin ability to X-Pack * refactor: 💡 move Discover drilldown example to X-Pack * feat: 🎸 add dashboard-to-url drilldown example * feat: 🎸 add new tab support for URL drilldown * feat: 🎸 add "hello world" drilldown example * docs: ✏️ add README * feat: 🎸 add getHref support * chore: 🤖 cleanup after moving examples to X-Pack * docs: ✏️ add to README.md info on how to find drilldowns * feat: 🎸 store events in .enhancements field * docs: ✏️ add comment to range trigger title * refactor: 💡 move Configurable interface into kibana_utils * chore: 🤖 simplify internal component types * refactor: 💡 move registerDrilldwon() to advanced_ui_actions * test: 💍 update functional test data * merge * docs: ✏️ make drilldown enhancement comment more general * fix: 🐛 return public type from registerAction() call * docs: ✏️ add comment to value click trigger title field * docs: ✏️ improve comment * fix: 🐛 use second argument of CollectConfigProps interface * fix: 🐛 add workaround for Firefox rendering issue See: https://github.com/elastic/kibana/pull/61219/#pullrequestreview-402903330 * chore: 🤖 delete unused file * fix: 🐛 import type from new location * style: 💄 make generic type variable name sconsistent * fix: 🐛 show "Create drilldown" only on dashboard * test: 💍 add extra unit test for root embeddable type * docs: ✏️ update generated docs * chore: 🤖 add example warnings to sample drilldowns * docs: ✏️ add links to example warnings * feat: 🎸 add URL drilldown validation and https:// prefixing * fix: 🐛 disable drilldowns for lens * refactor: 💡 remove PlaceContext from DrilldownDefinition * fix: 🐛 fix type check error * feat: 🎸 show warning message if embeddable not provided Co-authored-by: Anton Dosov Co-authored-by: Matt Kime Co-authored-by: Elastic Machine Co-authored-by: Andrea Del Rio Co-authored-by: ppisljar --- .github/CODEOWNERS | 1 + .i18nrc.json | 1 + ...na-plugin-plugins-data-public.esfilters.md | 1 + examples/ui_action_examples/public/index.ts | 3 +- examples/ui_action_examples/public/plugin.ts | 23 +- examples/ui_actions_explorer/public/app.tsx | 3 +- .../ui_actions_explorer/public/plugin.tsx | 16 +- .../public/overlays/flyout/flyout_service.tsx | 1 + src/dev/storybook/aliases.ts | 3 +- .../actions/clone_panel_action.tsx | 2 +- .../actions/replace_panel_action.tsx | 2 +- .../tests/dashboard_container.test.tsx | 2 +- src/plugins/dashboard/public/plugin.tsx | 6 +- .../public/actions/apply_filter_action.ts | 1 + src/plugins/data/public/index.ts | 2 + .../index_patterns/index_patterns.ts | 8 +- src/plugins/data/public/plugin.ts | 9 +- src/plugins/data/public/public.api.md | 94 +-- .../data/public/query/timefilter/index.ts | 2 +- .../timefilter/lib/change_time_filter.ts | 10 +- src/plugins/embeddable/public/bootstrap.ts | 4 + src/plugins/embeddable/public/index.ts | 20 +- .../public/lib/actions/edit_panel_action.ts | 2 +- .../public/lib/embeddables/embeddable.tsx | 20 +- .../embeddables/embeddable_action_storage.ts | 126 ---- .../public/lib/embeddables/i_embeddable.ts | 17 +- .../lib/panel/embeddable_panel.test.tsx | 14 +- .../public/lib/panel/embeddable_panel.tsx | 51 +- .../customize_title/customize_panel_action.ts | 8 +- .../panel_actions/inspect_panel_action.ts | 2 +- .../panel_actions/remove_panel_action.ts | 2 +- .../lib/panel/panel_header/panel_header.tsx | 21 +- .../public/lib/triggers/triggers.ts | 27 +- src/plugins/embeddable/public/mocks.ts | 15 +- .../create_state_container_react_helpers.ts | 69 +- .../common/state_containers/types.ts | 2 +- src/plugins/kibana_utils/index.ts | 20 + src/plugins/kibana_utils/public/index.ts | 2 + .../kibana_utils/public/ui/configurable.ts | 60 ++ src/plugins/kibana_utils/public/ui/index.ts | 20 + .../ui_actions/public/actions/action.ts | 28 +- .../public/actions/action_definition.ts | 72 -- .../public/actions/action_internal.test.ts | 33 + .../public/actions/action_internal.ts | 58 ++ .../public/actions/create_action.ts | 14 +- .../ui_actions/public/actions/index.ts | 1 + .../build_eui_context_menu_panels.tsx | 54 +- .../public/context_menu/open_context_menu.tsx | 6 +- src/plugins/ui_actions/public/index.ts | 8 +- src/plugins/ui_actions/public/mocks.ts | 14 +- src/plugins/ui_actions/public/plugin.ts | 7 +- .../public/service/ui_actions_service.test.ts | 48 +- .../public/service/ui_actions_service.ts | 77 ++- .../tests/execute_trigger_actions.test.ts | 10 +- .../public/tests/get_trigger_actions.test.ts | 9 +- .../get_trigger_compatible_actions.test.ts | 6 +- .../public/tests/test_samples/index.ts | 1 + .../public/triggers/select_range_trigger.ts | 4 +- .../public/triggers/trigger_internal.ts | 5 +- .../public/triggers/value_click_trigger.ts | 4 +- src/plugins/ui_actions/public/types.ts | 4 +- src/plugins/ui_actions/public/util/index.ts | 20 + .../ui_actions/public/util/presentable.ts | 65 ++ .../public/embeddable/visualize_embeddable.ts | 1 + .../functional/page_objects/dashboard_page.ts | 27 +- .../kbn_sample_panel_action/public/plugin.ts | 8 +- .../public/np_ready/public/plugin.tsx | 3 +- x-pack/.i18nrc.json | 2 + .../ui_actions_enhanced_examples/README.md | 37 +- .../ui_actions_enhanced_examples/kibana.json | 2 +- .../dashboard_hello_world_drilldown/README.md | 1 + .../dashboard_hello_world_drilldown/index.tsx | 60 ++ .../collect_config_container.tsx | 71 ++ .../discover_drilldown_config.tsx | 104 +++ .../discover_drilldown_config/i18n.ts | 14 + .../discover_drilldown_config/index.ts | 7 + .../components/index.ts | 7 + .../constants.ts | 7 + .../drilldown.tsx | 82 +++ .../dashboard_to_discover_drilldown/i18n.ts | 11 + .../dashboard_to_discover_drilldown/index.ts | 15 + .../dashboard_to_discover_drilldown/types.ts | 38 ++ .../dashboard_to_url_drilldown/index.tsx | 114 ++++ .../public/plugin.ts | 25 +- .../plugins/canvas/public/application.tsx | 4 +- .../action_wizard/action_wizard.scss | 5 - .../action_wizard/action_wizard.story.tsx | 24 +- .../action_wizard/action_wizard.test.tsx | 19 +- .../action_wizard/action_wizard.tsx | 112 +-- .../public/components/action_wizard/i18n.ts | 2 +- .../public/components/action_wizard/index.ts | 2 +- .../components/action_wizard/test_data.tsx | 218 +++--- .../public/components/index.ts} | 2 +- .../public/custom_time_range_action.tsx | 2 +- .../public/drilldowns/drilldown_definition.ts | 100 +++ .../public/drilldowns}/index.ts | 2 +- .../public/dynamic_actions/action_factory.ts | 55 ++ .../action_factory_definition.ts | 38 ++ .../dynamic_action_manager.test.ts | 635 ++++++++++++++++++ .../dynamic_actions/dynamic_action_manager.ts | 273 ++++++++ .../dynamic_action_manager_state.ts | 98 +++ .../dynamic_actions/dynamic_action_storage.ts | 80 +++ .../public/dynamic_actions/index.ts | 12 + .../public/dynamic_actions/types.ts | 20 + .../advanced_ui_actions/public/index.ts | 19 + .../advanced_ui_actions/public/mocks.ts | 73 ++ .../advanced_ui_actions/public/plugin.ts | 36 +- .../public/services/index.ts | 7 + .../ui_actions_service_enhancements.test.ts | 70 ++ .../ui_actions_service_enhancements.ts | 97 +++ .../advanced_ui_actions/public/types.ts | 3 + x-pack/plugins/dashboard_enhanced/README.md | 1 + x-pack/plugins/dashboard_enhanced/kibana.json | 8 + .../dashboard_enhanced/public/index.ts | 19 + .../dashboard_enhanced/public/mocks.ts | 27 + .../dashboard_enhanced/public/plugin.ts | 55 ++ .../flyout_create_drilldown.test.tsx | 144 ++++ .../flyout_create_drilldown.tsx | 86 +++ .../actions/flyout_create_drilldown/index.ts | 11 + .../flyout_edit_drilldown.test.tsx | 148 ++++ .../flyout_edit_drilldown.tsx | 74 ++ .../actions/flyout_edit_drilldown}/i18n.ts | 6 +- .../actions/flyout_edit_drilldown/index.tsx | 11 + .../flyout_edit_drilldown/menu_item.test.tsx | 39 ++ .../flyout_edit_drilldown/menu_item.tsx | 27 + .../services/drilldowns}/actions/index.ts | 0 .../drilldowns/actions/test_helpers.ts | 52 ++ .../dashboard_drilldowns_services.ts | 57 ++ .../components/collect_config_container.tsx | 164 +++++ .../dashboard_drilldown_config.story.tsx | 63 ++ .../dashboard_drilldown_config.test.tsx | 10 + .../dashboard_drilldown_config.tsx | 82 +++ .../dashboard_drilldown_config/i18n.ts | 28 + .../dashboard_drilldown_config/index.ts | 7 + .../components/i18n.ts | 16 + .../components/index.ts | 7 + .../constants.ts | 7 + .../drilldown.test.tsx | 363 ++++++++++ .../drilldown.tsx | 154 +++++ .../dashboard_to_dashboard_drilldown/i18n.ts | 11 + .../dashboard_to_dashboard_drilldown/index.ts | 15 + .../dashboard_to_dashboard_drilldown/types.ts | 21 + .../public/services/drilldowns/index.ts | 7 + .../public/services}/index.ts | 2 +- .../dashboard_enhanced/scripts/storybook.js | 13 + x-pack/plugins/drilldowns/kibana.json | 7 +- .../actions/flyout_create_drilldown/index.tsx | 52 -- .../actions/flyout_edit_drilldown/index.tsx | 72 -- ...nnected_flyout_manage_drilldowns.story.tsx | 43 ++ ...onnected_flyout_manage_drilldowns.test.tsx | 221 ++++++ .../connected_flyout_manage_drilldowns.tsx | 331 +++++++++ .../i18n.ts | 88 +++ .../index.ts | 7 + .../test_data.ts | 89 +++ .../drilldown_hello_bar.story.tsx | 16 +- .../drilldown_hello_bar.tsx | 58 +- .../components/drilldown_hello_bar/i18n.ts | 29 + .../drilldown_picker/drilldown_picker.tsx | 21 - .../flyout_create_drilldown.story.tsx | 24 - .../flyout_create_drilldown.tsx | 34 - .../flyout_drilldown_wizard.story.tsx | 70 ++ .../flyout_drilldown_wizard.tsx | 140 ++++ .../flyout_drilldown_wizard/i18n.ts | 42 ++ .../flyout_drilldown_wizard/index.ts | 7 + .../flyout_frame/flyout_frame.story.tsx | 7 + .../flyout_frame/flyout_frame.test.tsx | 4 +- .../components/flyout_frame/flyout_frame.tsx | 31 +- .../public/components/flyout_frame/i18n.ts | 6 +- .../flyout_list_manage_drilldowns.story.tsx | 22 + .../flyout_list_manage_drilldowns.tsx | 46 ++ .../i18n.ts} | 13 +- .../flyout_list_manage_drilldowns/index.ts | 7 + .../form_create_drilldown.story.tsx | 34 - .../form_create_drilldown.tsx | 52 -- .../form_drilldown_wizard.story.tsx | 29 + .../form_drilldown_wizard.test.tsx} | 28 +- .../form_drilldown_wizard.tsx | 79 +++ .../i18n.ts | 4 +- .../index.tsx | 2 +- .../components/list_manage_drilldowns/i18n.ts | 36 + .../list_manage_drilldowns/index.tsx | 7 + .../list_manage_drilldowns.story.tsx | 19 + .../list_manage_drilldowns.test.tsx | 70 ++ .../list_manage_drilldowns.tsx | 122 ++++ x-pack/plugins/drilldowns/public/index.ts | 8 +- x-pack/plugins/drilldowns/public/mocks.ts | 12 +- x-pack/plugins/drilldowns/public/plugin.ts | 55 +- .../public/service/drilldown_service.ts | 32 - x-pack/plugins/embeddable_enhanced/README.md | 1 + .../plugins/embeddable_enhanced/kibana.json | 7 + .../public/actions/index.ts | 7 + .../panel_notifications_action.test.ts | 75 +++ .../actions/panel_notifications_action.ts | 34 + .../embeddable_action_storage.test.ts | 186 ++--- .../embeddables/embeddable_action_storage.ts | 114 ++++ .../public/embeddables/index.ts | 8 + .../embeddables/is_enhanced_embeddable.ts | 14 + .../embeddable_enhanced/public/index.ts | 22 + .../embeddable_enhanced/public/mocks.ts | 27 + .../embeddable_enhanced/public/plugin.ts | 160 +++++ .../embeddable_enhanced/public/types.ts | 21 + x-pack/plugins/reporting/public/plugin.tsx | 3 +- .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - .../drilldowns/dashboard_drilldowns.ts | 176 +++++ .../apps/dashboard/drilldowns/index.ts | 13 + .../test/functional/apps/dashboard/index.ts | 1 + .../dashboard/drilldowns/data.json.gz | Bin 0 -> 2662 bytes .../dashboard/drilldowns/mappings.json | 244 +++++++ .../services/dashboard/drilldowns_manage.ts | 95 +++ .../functional/services/dashboard/index.ts | 8 + .../dashboard/panel_drilldown_actions.ts | 80 +++ x-pack/test/functional/services/index.ts | 6 + 213 files changed, 7817 insertions(+), 1190 deletions(-) delete mode 100644 src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.ts create mode 100644 src/plugins/kibana_utils/index.ts create mode 100644 src/plugins/kibana_utils/public/ui/configurable.ts create mode 100644 src/plugins/kibana_utils/public/ui/index.ts delete mode 100644 src/plugins/ui_actions/public/actions/action_definition.ts create mode 100644 src/plugins/ui_actions/public/actions/action_internal.test.ts create mode 100644 src/plugins/ui_actions/public/actions/action_internal.ts create mode 100644 src/plugins/ui_actions/public/util/index.ts create mode 100644 src/plugins/ui_actions/public/util/presentable.ts create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/README.md create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/collect_config_container.tsx create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/discover_drilldown_config.tsx create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/i18n.ts create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/index.ts create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/index.ts create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/constants.ts create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/i18n.ts create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/index.ts create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts create mode 100644 x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx rename x-pack/plugins/{drilldowns/public/components/drilldown_picker/index.tsx => advanced_ui_actions/public/components/index.ts} (87%) create mode 100644 x-pack/plugins/advanced_ui_actions/public/drilldowns/drilldown_definition.ts rename x-pack/plugins/{drilldowns/public/components/flyout_create_drilldown => advanced_ui_actions/public/drilldowns}/index.ts (84%) create mode 100644 x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory_definition.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager.test.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager_state.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_storage.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/dynamic_actions/index.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/dynamic_actions/types.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/mocks.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/services/index.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/services/ui_actions_service_enhancements.test.ts create mode 100644 x-pack/plugins/advanced_ui_actions/public/services/ui_actions_service_enhancements.ts create mode 100644 x-pack/plugins/dashboard_enhanced/README.md create mode 100644 x-pack/plugins/dashboard_enhanced/kibana.json create mode 100644 x-pack/plugins/dashboard_enhanced/public/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/mocks.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/plugin.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx rename x-pack/plugins/{drilldowns/public/components/flyout_create_drilldown => dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown}/i18n.ts (64%) create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/index.tsx create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.tsx rename x-pack/plugins/{drilldowns/public => dashboard_enhanced/public/services/drilldowns}/actions/index.ts (100%) create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/i18n.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/i18n.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/constants.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/i18n.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts create mode 100644 x-pack/plugins/dashboard_enhanced/public/services/drilldowns/index.ts rename x-pack/plugins/{drilldowns/public/service => dashboard_enhanced/public/services}/index.ts (86%) create mode 100644 x-pack/plugins/dashboard_enhanced/scripts/storybook.js delete mode 100644 x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx delete mode 100644 x-pack/plugins/drilldowns/public/actions/flyout_edit_drilldown/index.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/i18n.ts create mode 100644 x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/index.ts create mode 100644 x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/test_data.ts create mode 100644 x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/i18n.ts delete mode 100644 x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.tsx delete mode 100644 x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.story.tsx delete mode 100644 x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/i18n.ts create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/index.ts create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.story.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.tsx rename x-pack/plugins/drilldowns/public/components/{drilldown_picker/drilldown_picker.story.tsx => flyout_list_manage_drilldowns/i18n.ts} (52%) create mode 100644 x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/index.ts delete mode 100644 x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.story.tsx delete mode 100644 x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.story.tsx rename x-pack/plugins/drilldowns/public/components/{form_create_drilldown/form_create_drilldown.test.tsx => form_drilldown_wizard/form_drilldown_wizard.test.tsx} (60%) create mode 100644 x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx rename x-pack/plugins/drilldowns/public/components/{form_create_drilldown => form_drilldown_wizard}/i18n.ts (89%) rename x-pack/plugins/drilldowns/public/components/{form_create_drilldown => form_drilldown_wizard}/index.tsx (85%) create mode 100644 x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/i18n.ts create mode 100644 x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/index.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.story.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.test.tsx create mode 100644 x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.tsx delete mode 100644 x-pack/plugins/drilldowns/public/service/drilldown_service.ts create mode 100644 x-pack/plugins/embeddable_enhanced/README.md create mode 100644 x-pack/plugins/embeddable_enhanced/kibana.json create mode 100644 x-pack/plugins/embeddable_enhanced/public/actions/index.ts create mode 100644 x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.test.ts create mode 100644 x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.ts rename {src/plugins/embeddable/public/lib => x-pack/plugins/embeddable_enhanced/public}/embeddables/embeddable_action_storage.test.ts (72%) create mode 100644 x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts create mode 100644 x-pack/plugins/embeddable_enhanced/public/embeddables/index.ts create mode 100644 x-pack/plugins/embeddable_enhanced/public/embeddables/is_enhanced_embeddable.ts create mode 100644 x-pack/plugins/embeddable_enhanced/public/index.ts create mode 100644 x-pack/plugins/embeddable_enhanced/public/mocks.ts create mode 100644 x-pack/plugins/embeddable_enhanced/public/plugin.ts create mode 100644 x-pack/plugins/embeddable_enhanced/public/types.ts create mode 100644 x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts create mode 100644 x-pack/test/functional/apps/dashboard/drilldowns/index.ts create mode 100644 x-pack/test/functional/es_archives/dashboard/drilldowns/data.json.gz create mode 100644 x-pack/test/functional/es_archives/dashboard/drilldowns/mappings.json create mode 100644 x-pack/test/functional/services/dashboard/drilldowns_manage.ts create mode 100644 x-pack/test/functional/services/dashboard/index.ts create mode 100644 x-pack/test/functional/services/dashboard/panel_drilldown_actions.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b4692a4ddb3b7..a008fa7ea9239 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,6 +3,7 @@ # For more info, see https://help.github.com/articles/about-codeowners/ # App +/x-pack/plugins/dashboard_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app /src/legacy/server/url_shortening/ @elastic/kibana-app diff --git a/.i18nrc.json b/.i18nrc.json index b04c02f6b2265..be3c043b6e52f 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -8,6 +8,7 @@ "data": "src/plugins/data", "embeddableApi": "src/plugins/embeddable", "embeddableExamples": "examples/embeddable_examples", + "uiActionsExamples": "examples/ui_action_examples", "share": "src/plugins/share", "home": "src/plugins/home", "charts": "src/plugins/charts", diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md index 7fd65e5db35f3..37142cf1794c3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -49,6 +49,7 @@ esFilters: { generateFilters: typeof generateFilters; onlyDisabledFiltersChanged: (newFilters?: import("../common").Filter[] | undefined, oldFilters?: import("../common").Filter[] | undefined) => boolean; changeTimeFilter: typeof changeTimeFilter; + convertRangeFilterToTimeRangeString: typeof convertRangeFilterToTimeRangeString; mapAndFlattenFilters: (filters: import("../common").Filter[]) => import("../common").Filter[]; extractTimeFilter: typeof extractTimeFilter; } diff --git a/examples/ui_action_examples/public/index.ts b/examples/ui_action_examples/public/index.ts index 88a36d278e256..5b08192a1196e 100644 --- a/examples/ui_action_examples/public/index.ts +++ b/examples/ui_action_examples/public/index.ts @@ -18,9 +18,8 @@ */ import { UiActionExamplesPlugin } from './plugin'; -import { PluginInitializer } from '../../../src/core/public'; -export const plugin: PluginInitializer = () => new UiActionExamplesPlugin(); +export const plugin = () => new UiActionExamplesPlugin(); export { HELLO_WORLD_TRIGGER_ID } from './hello_world_trigger'; export { ACTION_HELLO_WORLD } from './hello_world_action'; diff --git a/examples/ui_action_examples/public/plugin.ts b/examples/ui_action_examples/public/plugin.ts index c47746d4b3fd6..3a9f673261e33 100644 --- a/examples/ui_action_examples/public/plugin.ts +++ b/examples/ui_action_examples/public/plugin.ts @@ -17,15 +17,19 @@ * under the License. */ -import { Plugin, CoreSetup } from '../../../src/core/public'; -import { UiActionsSetup } from '../../../src/plugins/ui_actions/public'; +import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public'; +import { UiActionsSetup, UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { createHelloWorldAction, ACTION_HELLO_WORLD } from './hello_world_action'; import { helloWorldTrigger, HELLO_WORLD_TRIGGER_ID } from './hello_world_trigger'; -interface UiActionExamplesSetupDependencies { +export interface UiActionExamplesSetupDependencies { uiActions: UiActionsSetup; } +export interface UiActionExamplesStartDependencies { + uiActions: UiActionsStart; +} + declare module '../../../src/plugins/ui_actions/public' { export interface TriggerContextMapping { [HELLO_WORLD_TRIGGER_ID]: {}; @@ -37,8 +41,12 @@ declare module '../../../src/plugins/ui_actions/public' { } export class UiActionExamplesPlugin - implements Plugin { - public setup(core: CoreSetup, { uiActions }: UiActionExamplesSetupDependencies) { + implements + Plugin { + public setup( + core: CoreSetup, + { uiActions }: UiActionExamplesSetupDependencies + ) { uiActions.registerTrigger(helloWorldTrigger); const helloWorldAction = createHelloWorldAction(async () => ({ @@ -46,9 +54,10 @@ export class UiActionExamplesPlugin })); uiActions.registerAction(helloWorldAction); - uiActions.attachAction(helloWorldTrigger.id, helloWorldAction); + uiActions.addTriggerAction(helloWorldTrigger.id, helloWorldAction); } - public start() {} + public start(core: CoreStart, plugins: UiActionExamplesStartDependencies) {} + public stop() {} } diff --git a/examples/ui_actions_explorer/public/app.tsx b/examples/ui_actions_explorer/public/app.tsx index 462f5c3bf88ba..f08b8bb29bdd3 100644 --- a/examples/ui_actions_explorer/public/app.tsx +++ b/examples/ui_actions_explorer/public/app.tsx @@ -95,8 +95,7 @@ const ActionsExplorer = ({ uiActionsApi, openModal }: Props) => { ); }, }); - uiActionsApi.registerAction(dynamicAction); - uiActionsApi.attachAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); + uiActionsApi.addTriggerAction(HELLO_WORLD_TRIGGER_ID, dynamicAction); setConfirmationText( `You've successfully added a new action: ${dynamicAction.getDisplayName( {} diff --git a/examples/ui_actions_explorer/public/plugin.tsx b/examples/ui_actions_explorer/public/plugin.tsx index f1895905a45e1..de86b51aee3a8 100644 --- a/examples/ui_actions_explorer/public/plugin.tsx +++ b/examples/ui_actions_explorer/public/plugin.tsx @@ -79,21 +79,21 @@ export class UiActionsExplorerPlugin implements Plugin (await startServices)[1].uiActions) ); - deps.uiActions.attachAction( + deps.uiActions.addTriggerAction( USER_TRIGGER, createEditUserAction(async () => (await startServices)[0].overlays.openModal) ); - deps.uiActions.attachAction(COUNTRY_TRIGGER, viewInMapsAction); - deps.uiActions.attachAction(COUNTRY_TRIGGER, lookUpWeatherAction); - deps.uiActions.attachAction(COUNTRY_TRIGGER, showcasePluggability); - deps.uiActions.attachAction(PHONE_TRIGGER, makePhoneCallAction); - deps.uiActions.attachAction(PHONE_TRIGGER, showcasePluggability); - deps.uiActions.attachAction(USER_TRIGGER, showcasePluggability); + deps.uiActions.addTriggerAction(COUNTRY_TRIGGER, viewInMapsAction); + deps.uiActions.addTriggerAction(COUNTRY_TRIGGER, lookUpWeatherAction); + deps.uiActions.addTriggerAction(COUNTRY_TRIGGER, showcasePluggability); + deps.uiActions.addTriggerAction(PHONE_TRIGGER, makePhoneCallAction); + deps.uiActions.addTriggerAction(PHONE_TRIGGER, showcasePluggability); + deps.uiActions.addTriggerAction(USER_TRIGGER, showcasePluggability); core.application.register({ id: 'uiActionsExplorer', diff --git a/src/core/public/overlays/flyout/flyout_service.tsx b/src/core/public/overlays/flyout/flyout_service.tsx index b609b2ce1d741..444430175d4f2 100644 --- a/src/core/public/overlays/flyout/flyout_service.tsx +++ b/src/core/public/overlays/flyout/flyout_service.tsx @@ -91,6 +91,7 @@ export interface OverlayFlyoutStart { export interface OverlayFlyoutOpenOptions { className?: string; closeButtonAriaLabel?: string; + ownFocus?: boolean; 'data-test-subj'?: string; } diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 4dc930dae3e25..0e91f0a214a45 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -18,12 +18,13 @@ */ export const storybookAliases = { + advanced_ui_actions: 'x-pack/plugins/advanced_ui_actions/scripts/storybook.js', apm: 'x-pack/plugins/apm/scripts/storybook.js', canvas: 'x-pack/legacy/plugins/canvas/scripts/storybook_new.js', codeeditor: 'src/plugins/kibana_react/public/code_editor/scripts/storybook.ts', + dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/scripts/storybook.js', drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js', embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', siem: 'x-pack/plugins/siem/scripts/storybook.js', - ui_actions: 'x-pack/plugins/advanced_ui_actions/scripts/storybook.js', }; diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx index 4d15e7e899fa8..ff4e50ba8c327 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.tsx @@ -39,7 +39,7 @@ export interface ClonePanelActionContext { export class ClonePanelAction implements ActionByType { public readonly type = ACTION_CLONE_PANEL; public readonly id = ACTION_CLONE_PANEL; - public order = 11; + public order = 45; constructor(private core: CoreStart) {} diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx index ddc255295e89b..5526af2f83850 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_action.tsx @@ -37,7 +37,7 @@ export interface ReplacePanelActionContext { export class ReplacePanelAction implements ActionByType { public readonly type = ACTION_REPLACE_PANEL; public readonly id = ACTION_REPLACE_PANEL; - public order = 11; + public order = 3; constructor( private core: CoreStart, diff --git a/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx index 5dab21ff671b4..40231de7597f1 100644 --- a/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx @@ -46,7 +46,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => { const editModeAction = createEditModeAction(); uiActionsSetup.registerAction(editModeAction); - uiActionsSetup.attachAction(CONTEXT_MENU_TRIGGER, editModeAction); + uiActionsSetup.addTriggerAction(CONTEXT_MENU_TRIGGER, editModeAction); setup.registerEmbeddableFactory( CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory((() => null) as any, {} as any) diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 7de054f2eaa9c..b28822120b31e 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -136,7 +136,7 @@ export class DashboardPlugin ): Setup { const expandPanelAction = new ExpandPanelAction(); uiActions.registerAction(expandPanelAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction.id); const startServices = core.getStartServices(); if (share) { @@ -310,11 +310,11 @@ export class DashboardPlugin plugins.embeddable.getEmbeddableFactories ); uiActions.registerAction(changeViewAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction.id); const clonePanelAction = new ClonePanelAction(core); uiActions.registerAction(clonePanelAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction); + uiActions.attachAction(CONTEXT_MENU_TRIGGER, clonePanelAction.id); const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, diff --git a/src/plugins/data/public/actions/apply_filter_action.ts b/src/plugins/data/public/actions/apply_filter_action.ts index bd20c6f632a3a..ebaac6b745bec 100644 --- a/src/plugins/data/public/actions/apply_filter_action.ts +++ b/src/plugins/data/public/actions/apply_filter_action.ts @@ -42,6 +42,7 @@ export function createFilterAction( return createAction({ type: ACTION_GLOBAL_APPLY_FILTER, id: ACTION_GLOBAL_APPLY_FILTER, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 75deff23ce20d..ebc794ed7e595 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -59,6 +59,7 @@ import { changeTimeFilter, mapAndFlattenFilters, extractTimeFilter, + convertRangeFilterToTimeRangeString, } from './query'; // Filter helpers namespace: @@ -96,6 +97,7 @@ export const esFilters = { onlyDisabledFiltersChanged, changeTimeFilter, + convertRangeFilterToTimeRangeString, mapAndFlattenFilters, extractTimeFilter, }; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts index b5d66a6aab60a..73d5aeaf30710 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts @@ -37,10 +37,14 @@ const indexPatternCache = createIndexPatternCache(); type IndexPatternCachedFieldType = 'id' | 'title'; +export interface IndexPatternSavedObjectAttrs { + title: string; +} + export class IndexPatternsService { private config: IUiSettingsClient; private savedObjectsClient: SavedObjectsClientContract; - private savedObjectsCache?: Array>> | null; + private savedObjectsCache?: Array> | null; private apiClient: IndexPatternsApiClient; ensureDefaultIndexPattern: EnsureDefaultIndexPattern; @@ -53,7 +57,7 @@ export class IndexPatternsService { private async refreshSavedObjectsCache() { this.savedObjectsCache = ( - await this.savedObjectsClient.find>({ + await this.savedObjectsClient.find({ type: 'index-pattern', fields: ['title'], perPage: 10000, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index f3a88287313a0..d822e96d0a129 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -126,12 +126,12 @@ export class DataPublicPlugin implements Plugin boolean; changeTimeFilter: typeof changeTimeFilter; + convertRangeFilterToTimeRangeString: typeof convertRangeFilterToTimeRangeString; mapAndFlattenFilters: (filters: import("../common").Filter[]) => import("../common").Filter[]; extractTimeFilter: typeof extractTimeFilter; }; @@ -1783,52 +1784,53 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/common/es_query/filters/match_all_filter.ts:28:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:65:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:135:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:135:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:135:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:135:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:377:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:378:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "changeTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "convertRangeFilterToTimeRangeString" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "extractTimeFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:137:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:137:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:137:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:137:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:179:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:238:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:377:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:379:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:380:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/query/timefilter/index.ts b/src/plugins/data/public/query/timefilter/index.ts index 034af03842ab8..a5885a59f60ed 100644 --- a/src/plugins/data/public/query/timefilter/index.ts +++ b/src/plugins/data/public/query/timefilter/index.ts @@ -23,5 +23,5 @@ export * from './types'; export { Timefilter, TimefilterContract } from './timefilter'; export { TimeHistory, TimeHistoryContract } from './time_history'; export { getTime, calculateBounds } from './get_time'; -export { changeTimeFilter } from './lib/change_time_filter'; +export { changeTimeFilter, convertRangeFilterToTimeRangeString } from './lib/change_time_filter'; export { extractTimeFilter } from './lib/extract_time_filter'; diff --git a/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts b/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts index 8da83580ef5d6..cbbf2f2754312 100644 --- a/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts +++ b/src/plugins/data/public/query/timefilter/lib/change_time_filter.ts @@ -20,7 +20,7 @@ import moment from 'moment'; import { keys } from 'lodash'; import { TimefilterContract } from '../../timefilter'; -import { RangeFilter } from '../../../../common'; +import { RangeFilter, TimeRange } from '../../../../common'; export function convertRangeFilterToTimeRange(filter: RangeFilter) { const key = keys(filter.range)[0]; @@ -32,6 +32,14 @@ export function convertRangeFilterToTimeRange(filter: RangeFilter) { }; } +export function convertRangeFilterToTimeRangeString(filter: RangeFilter): TimeRange { + const { from, to } = convertRangeFilterToTimeRange(filter); + return { + from: from?.toISOString(), + to: to?.toISOString(), + }; +} + export function changeTimeFilter(timeFilter: TimefilterContract, filter: RangeFilter) { timeFilter.setTime(convertRangeFilterToTimeRange(filter)); } diff --git a/src/plugins/embeddable/public/bootstrap.ts b/src/plugins/embeddable/public/bootstrap.ts index c8c4f0b95c458..33cf210763b10 100644 --- a/src/plugins/embeddable/public/bootstrap.ts +++ b/src/plugins/embeddable/public/bootstrap.ts @@ -31,12 +31,15 @@ import { ACTION_EDIT_PANEL, FilterActionContext, ACTION_APPLY_FILTER, + panelNotificationTrigger, + PANEL_NOTIFICATION_TRIGGER, } from './lib'; declare module '../../ui_actions/public' { export interface TriggerContextMapping { [CONTEXT_MENU_TRIGGER]: EmbeddableContext; [PANEL_BADGE_TRIGGER]: EmbeddableContext; + [PANEL_NOTIFICATION_TRIGGER]: EmbeddableContext; } export interface ActionContextMapping { @@ -56,6 +59,7 @@ declare module '../../ui_actions/public' { export const bootstrap = (uiActions: UiActionsSetup) => { uiActions.registerTrigger(contextMenuTrigger); uiActions.registerTrigger(panelBadgeTrigger); + uiActions.registerTrigger(panelNotificationTrigger); const actionApplyFilter = createFilterAction(); diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 5ee66f9d19ac0..e61ad2a6eefed 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -23,23 +23,24 @@ import { PluginInitializerContext } from 'src/core/public'; import { EmbeddablePublicPlugin } from './plugin'; export { - Adapters, ACTION_ADD_PANEL, - AddPanelAction, ACTION_APPLY_FILTER, + ACTION_EDIT_PANEL, + Adapters, + AddPanelAction, Container, ContainerInput, ContainerOutput, CONTEXT_MENU_TRIGGER, contextMenuTrigger, - ACTION_EDIT_PANEL, + defaultEmbeddableFactoryProvider, EditPanelAction, Embeddable, EmbeddableChildPanel, EmbeddableChildPanelProps, EmbeddableContext, - EmbeddableFactoryDefinition, EmbeddableFactory, + EmbeddableFactoryDefinition, EmbeddableFactoryNotFoundError, EmbeddableFactoryRenderer, EmbeddableInput, @@ -57,6 +58,8 @@ export { OutputSpec, PANEL_BADGE_TRIGGER, panelBadgeTrigger, + PANEL_NOTIFICATION_TRIGGER, + panelNotificationTrigger, PanelNotFoundError, PanelState, PropertySpec, @@ -64,10 +67,17 @@ export { withEmbeddableSubscription, SavedObjectEmbeddableInput, isSavedObjectEmbeddableInput, + isRangeSelectTriggerContext, + isValueClickTriggerContext, } from './lib'; export function plugin(initializerContext: PluginInitializerContext) { return new EmbeddablePublicPlugin(initializerContext); } -export { EmbeddableSetup, EmbeddableStart } from './plugin'; +export { + EmbeddableSetup, + EmbeddableStart, + EmbeddableSetupDependencies, + EmbeddableStartDependencies, +} from './plugin'; diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 0abbc25ff49a6..d57867900c24b 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -34,7 +34,7 @@ interface ActionContext { export class EditPanelAction implements Action { public readonly type = ACTION_EDIT_PANEL; public readonly id = ACTION_EDIT_PANEL; - public order = 15; + public order = 50; constructor( private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'], diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index a135484ff61be..9c544e86e189a 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -16,14 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { isEqual, cloneDeep } from 'lodash'; + +import { cloneDeep, isEqual } from 'lodash'; import * as Rx from 'rxjs'; -import { Adapters } from '../types'; +import { Adapters, ViewMode } from '../types'; import { IContainer } from '../containers'; -import { IEmbeddable, EmbeddableInput, EmbeddableOutput } from './i_embeddable'; -import { ViewMode } from '../types'; +import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable'; import { TriggerContextMapping } from '../ui_actions'; -import { EmbeddableActionStorage } from './embeddable_action_storage'; function getPanelTitle(input: EmbeddableInput, output: EmbeddableOutput) { return input.hidePanelTitles ? '' : input.title === undefined ? output.defaultTitle : input.title; @@ -33,6 +32,10 @@ export abstract class Embeddable< TEmbeddableInput extends EmbeddableInput = EmbeddableInput, TEmbeddableOutput extends EmbeddableOutput = EmbeddableOutput > implements IEmbeddable { + static runtimeId: number = 0; + + public readonly runtimeId = Embeddable.runtimeId++; + public readonly parent?: IContainer; public readonly isContainer: boolean = false; public abstract readonly type: string; @@ -51,11 +54,6 @@ 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 = { @@ -158,8 +156,10 @@ export abstract class Embeddable< */ public destroy(): void { this.destoyed = true; + this.input$.complete(); this.output$.complete(); + if (this.parentSubscription) { this.parentSubscription.unsubscribe(); } diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.ts deleted file mode 100644 index 520f92840c5f9..0000000000000 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.ts +++ /dev/null @@ -1,126 +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 { 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 9a3e49e497962..c16698a5f8637 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -36,9 +36,9 @@ export interface EmbeddableInput { hidePanelTitles?: boolean; /** - * Reserved key for `ui_actions` events. + * Reserved key for enhancements added by other plugins. */ - events?: unknown; + enhancements?: unknown; /** * List of action IDs that this embeddable should not render. @@ -91,6 +91,19 @@ export interface IEmbeddable< **/ readonly id: string; + /** + * Unique ID an embeddable is assigned each time it is initialized. This ID + * is different for different instances of the same embeddable. For example, + * if the same dashboard is rendered twice on the screen, all embeddable + * instances will have a unique `runtimeId`. + */ + readonly runtimeId?: number; + + /** + * Extra abilities added to Embeddable by `*_enhanced` plugins. + */ + enhancements?: object; + /** * A functional representation of the isContainer variable, but helpful for typescript to * know the shape if this returns true diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 49b6d7803a200..9dd4c74c624d9 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -45,7 +45,7 @@ import { inspectorPluginMock } from '../../../../inspector/public/mocks'; import { EuiBadge } from '@elastic/eui'; import { embeddablePluginMock } from '../../mocks'; -const actionRegistry = new Map>(); +const actionRegistry = new Map(); const triggerRegistry = new Map(); const { setup, doStart } = embeddablePluginMock.createInstance(); @@ -214,13 +214,17 @@ const renderInEditModeAndOpenContextMenu = async ( }; test('HelloWorldContainer in edit mode hides disabledActions', async () => { - const action: Action = { + const action = { id: 'FOO', type: 'FOO' as ActionType, getIconType: () => undefined, getDisplayName: () => 'foo', isCompatible: async () => true, execute: async () => {}, + order: 10, + getHref: () => { + return Promise.resolve(undefined); + }, }; const getActions = () => Promise.resolve([action]); @@ -246,13 +250,17 @@ test('HelloWorldContainer in edit mode hides disabledActions', async () => { }); test('HelloWorldContainer hides disabled badges', async () => { - const action: Action = { + const action = { id: 'BAR', type: 'BAR' as ActionType, getIconType: () => undefined, getDisplayName: () => 'bar', isCompatible: async () => true, execute: async () => {}, + order: 10, + getHref: () => { + return Promise.resolve(undefined); + }, }; const getActions = () => Promise.resolve([action]); diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index c43359382a33d..36ddfb49b0312 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -25,7 +25,12 @@ import { CoreStart, OverlayStart } from '../../../../../core/public'; import { toMountPoint } from '../../../../kibana_react/public'; import { Start as InspectorStartContract } from '../inspector'; -import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, EmbeddableContext } from '../triggers'; +import { + CONTEXT_MENU_TRIGGER, + PANEL_BADGE_TRIGGER, + PANEL_NOTIFICATION_TRIGGER, + EmbeddableContext, +} from '../triggers'; import { IEmbeddable } from '../embeddables/i_embeddable'; import { ViewMode } from '../types'; @@ -38,6 +43,14 @@ import { EditPanelAction } from '../actions'; import { CustomizePanelModal } from './panel_header/panel_actions/customize_title/customize_panel_modal'; import { EmbeddableStart } from '../../plugin'; +const sortByOrderField = ( + { order: orderA }: { order?: number }, + { order: orderB }: { order?: number } +) => (orderB || 0) - (orderA || 0); + +const removeById = (disabledActions: string[]) => ({ id }: { id: string }) => + disabledActions.indexOf(id) === -1; + interface Props { embeddable: IEmbeddable; getActions: UiActionsService['getTriggerCompatibleActions']; @@ -58,6 +71,7 @@ interface State { hidePanelTitles: boolean; closeContextMenu: boolean; badges: Array>; + notifications: Array>; } export class EmbeddablePanel extends React.Component { @@ -83,6 +97,7 @@ export class EmbeddablePanel extends React.Component { hidePanelTitles, closeContextMenu: false, badges: [], + notifications: [], }; this.embeddableRoot = React.createRef(); @@ -104,6 +119,22 @@ export class EmbeddablePanel extends React.Component { }); } + private async refreshNotifications() { + let notifications = await this.props.getActions(PANEL_NOTIFICATION_TRIGGER, { + embeddable: this.props.embeddable, + }); + if (!this.mounted) return; + + const { disabledActions } = this.props.embeddable.getInput(); + if (disabledActions) { + notifications = notifications.filter(badge => disabledActions.indexOf(badge.id) === -1); + } + + this.setState({ + notifications, + }); + } + public UNSAFE_componentWillMount() { this.mounted = true; const { embeddable } = this.props; @@ -116,6 +147,7 @@ export class EmbeddablePanel extends React.Component { }); this.refreshBadges(); + this.refreshNotifications(); } }); @@ -127,6 +159,7 @@ export class EmbeddablePanel extends React.Component { }); this.refreshBadges(); + this.refreshNotifications(); } }); } @@ -176,6 +209,7 @@ export class EmbeddablePanel extends React.Component { closeContextMenu={this.state.closeContextMenu} title={title} badges={this.state.badges} + notifications={this.state.notifications} embeddable={this.props.embeddable} headerId={headerId} /> @@ -202,13 +236,14 @@ export class EmbeddablePanel extends React.Component { }; private getActionContextMenuPanel = async () => { - let actions = await this.props.getActions(CONTEXT_MENU_TRIGGER, { + let regularActions = await this.props.getActions(CONTEXT_MENU_TRIGGER, { embeddable: this.props.embeddable, }); const { disabledActions } = this.props.embeddable.getInput(); if (disabledActions) { - actions = actions.filter(action => disabledActions.indexOf(action.id) === -1); + const removeDisabledActions = removeById(disabledActions); + regularActions = regularActions.filter(removeDisabledActions); } const createGetUserData = (overlays: OverlayStart) => @@ -247,16 +282,10 @@ export class EmbeddablePanel extends React.Component { new EditPanelAction(this.props.getEmbeddableFactory, this.props.application), ]; - const sorted = actions - .concat(extraActions) - .sort((a: Action, b: Action) => { - const bOrder = b.order || 0; - const aOrder = a.order || 0; - return bOrder - aOrder; - }); + const sortedActions = [...regularActions, ...extraActions].sort(sortByOrderField); return await buildContextMenuForActions({ - actions: sorted, + actions: sortedActions, actionContext: { embeddable: this.props.embeddable }, closeMenu: this.closeMyContextMenuPanel, }); diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts index c0e43c0538833..36957c3b79491 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts @@ -33,15 +33,13 @@ interface ActionContext { export class CustomizePanelTitleAction implements Action { public readonly type = ACTION_CUSTOMIZE_PANEL; public id = ACTION_CUSTOMIZE_PANEL; - public order = 10; + public order = 40; - constructor(private readonly getDataFromUser: GetUserData) { - this.order = 10; - } + constructor(private readonly getDataFromUser: GetUserData) {} public getDisplayName() { return i18n.translate('embeddableApi.customizePanel.action.displayName', { - defaultMessage: 'Customize panel', + defaultMessage: 'Edit panel title', }); } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts index d04f35715537c..ae9645767b267 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts @@ -31,7 +31,7 @@ interface ActionContext { export class InspectPanelAction implements Action { public readonly type = ACTION_INSPECT_PANEL; public readonly id = ACTION_INSPECT_PANEL; - public order = 10; + public order = 20; constructor(private readonly inspector: InspectorStartContract) {} diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts index ee7948f3d6a4a..a6d4128f3f106 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts @@ -41,7 +41,7 @@ function hasExpandedPanelInput( export class RemovePanelAction implements Action { public readonly type = REMOVE_PANEL_ACTION; public readonly id = REMOVE_PANEL_ACTION; - public order = 5; + public order = 1; constructor() {} diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index 99516a1d21d6f..35a10ed848e83 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -23,6 +23,7 @@ import { EuiIcon, EuiToolTip, EuiScreenReaderOnly, + EuiNotificationBadge, } from '@elastic/eui'; import classNames from 'classnames'; import React from 'react'; @@ -38,6 +39,7 @@ export interface PanelHeaderProps { getActionContextMenuPanel: () => Promise; closeContextMenu: boolean; badges: Array>; + notifications: Array>; embeddable: IEmbeddable; headerId?: string; } @@ -56,6 +58,22 @@ function renderBadges(badges: Array>, embeddable: IEmb )); } +function renderNotifications( + notifications: Array>, + embeddable: IEmbeddable +) { + return notifications.map(notification => ( + notification.execute({ embeddable })} + > + {notification.getDisplayName({ embeddable })} + + )); +} + function renderTooltip(description: string) { return ( description !== '' && ( @@ -88,6 +106,7 @@ export function PanelHeader({ getActionContextMenuPanel, closeContextMenu, badges, + notifications, embeddable, headerId, }: PanelHeaderProps) { @@ -147,7 +166,7 @@ export function PanelHeader({ )} {renderBadges(badges, embeddable)} - + {renderNotifications(notifications, embeddable)} { + embeddable?: T; timeFieldName?: string; data: { data: Array<{ @@ -39,8 +39,12 @@ export interface ValueClickTriggerContext { }; } -export interface RangeSelectTriggerContext { - embeddable?: IEmbeddable; +export const isValueClickTriggerContext = ( + context: ValueClickTriggerContext | RangeSelectTriggerContext +): context is ValueClickTriggerContext => context.data && 'data' in context.data; + +export interface RangeSelectTriggerContext { + embeddable?: T; timeFieldName?: string; data: { table: KibanaDatatable; @@ -49,6 +53,10 @@ export interface RangeSelectTriggerContext { }; } +export const isRangeSelectTriggerContext = ( + context: ValueClickTriggerContext | RangeSelectTriggerContext +): context is RangeSelectTriggerContext => context.data && 'range' in context.data; + export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = { id: CONTEXT_MENU_TRIGGER, @@ -60,5 +68,12 @@ export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; export const panelBadgeTrigger: Trigger<'PANEL_BADGE_TRIGGER'> = { id: PANEL_BADGE_TRIGGER, title: 'Panel badges', - description: 'Actions appear in title bar when an embeddable loads in a panel', + description: 'Actions appear in title bar when an embeddable loads in a panel.', +}; + +export const PANEL_NOTIFICATION_TRIGGER = 'PANEL_NOTIFICATION_TRIGGER'; +export const panelNotificationTrigger: Trigger<'PANEL_NOTIFICATION_TRIGGER'> = { + id: PANEL_NOTIFICATION_TRIGGER, + title: 'Panel notifications', + description: 'Actions appear in top-right corner of a panel.', }; diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts index 65b15f3a7614f..f5487c381cfcb 100644 --- a/src/plugins/embeddable/public/mocks.ts +++ b/src/plugins/embeddable/public/mocks.ts @@ -16,7 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { EmbeddableStart, EmbeddableSetup } from '.'; +import { + EmbeddableStart, + EmbeddableSetup, + EmbeddableSetupDependencies, + EmbeddableStartDependencies, +} from '.'; import { EmbeddablePublicPlugin } from './plugin'; import { coreMock } from '../../../core/public/mocks'; @@ -45,14 +50,14 @@ const createStartContract = (): Start => { return startContract; }; -const createInstance = () => { +const createInstance = (setupPlugins: Partial = {}) => { const plugin = new EmbeddablePublicPlugin({} as any); const setup = plugin.setup(coreMock.createSetup(), { - uiActions: uiActionsPluginMock.createSetupContract(), + uiActions: setupPlugins.uiActions || uiActionsPluginMock.createSetupContract(), }); - const doStart = () => + const doStart = (startPlugins: Partial = {}) => plugin.start(coreMock.createStart(), { - uiActions: uiActionsPluginMock.createStartContract(), + uiActions: startPlugins.uiActions || uiActionsPluginMock.createStartContract(), inspector: inspectorPluginMock.createStartContract(), }); return { diff --git a/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts index 36903f2d7c90f..90823359359a1 100644 --- a/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts @@ -24,15 +24,58 @@ import { Comparator, Connect, StateContainer, UnboxState } from './types'; const { useContext, useLayoutEffect, useRef, createElement: h } = React; +/** + * Returns the latest state of a state container. + * + * @param container State container which state to track. + */ +export const useContainerState = >( + container: Container +): UnboxState => useObservable(container.state$, container.get()); + +/** + * Apply selector to state container to extract only needed information. Will + * re-render your component only when the section changes. + * + * @param container State container which state to track. + * @param selector Function used to pick parts of state. + * @param comparator Comparator function used to memoize previous result, to not + * re-render React component if state did not change. By default uses + * `fast-deep-equal` package. + */ +export const useContainerSelector = , Result>( + container: Container, + selector: (state: UnboxState) => Result, + comparator: Comparator = defaultComparator +): Result => { + const { state$, get } = container; + const lastValueRef = useRef(get()); + const [value, setValue] = React.useState(() => { + const newValue = selector(get()); + lastValueRef.current = newValue; + return newValue; + }); + useLayoutEffect(() => { + const subscription = state$.subscribe((currentState: UnboxState) => { + const newValue = selector(currentState); + if (!comparator(lastValueRef.current, newValue)) { + lastValueRef.current = newValue; + setValue(newValue); + } + }); + return () => subscription.unsubscribe(); + }, [state$, comparator]); + return value; +}; + export const createStateContainerReactHelpers = >() => { const context = React.createContext(null as any); const useContainer = (): Container => useContext(context); const useState = (): UnboxState => { - const { state$, get } = useContainer(); - const value = useObservable(state$, get()); - return value; + const container = useContainer(); + return useContainerState(container); }; const useTransitions: () => Container['transitions'] = () => useContainer().transitions; @@ -41,24 +84,8 @@ export const createStateContainerReactHelpers = ) => Result, comparator: Comparator = defaultComparator ): Result => { - const { state$, get } = useContainer(); - const lastValueRef = useRef(get()); - const [value, setValue] = React.useState(() => { - const newValue = selector(get()); - lastValueRef.current = newValue; - return newValue; - }); - useLayoutEffect(() => { - const subscription = state$.subscribe((currentState: UnboxState) => { - const newValue = selector(currentState); - if (!comparator(lastValueRef.current, newValue)) { - lastValueRef.current = newValue; - setValue(newValue); - } - }); - return () => subscription.unsubscribe(); - }, [state$, comparator]); - return value; + const container = useContainer(); + return useContainerSelector(container, selector, comparator); }; const connect: Connect> = mapStateToProp => component => props => diff --git a/src/plugins/kibana_utils/common/state_containers/types.ts b/src/plugins/kibana_utils/common/state_containers/types.ts index 26a29bc470e8a..29ffa4cd486b5 100644 --- a/src/plugins/kibana_utils/common/state_containers/types.ts +++ b/src/plugins/kibana_utils/common/state_containers/types.ts @@ -43,7 +43,7 @@ export interface BaseStateContainer { export interface StateContainer< State extends BaseState, - PureTransitions extends object, + PureTransitions extends object = object, PureSelectors extends object = {} > extends BaseStateContainer { transitions: Readonly>; diff --git a/src/plugins/kibana_utils/index.ts b/src/plugins/kibana_utils/index.ts new file mode 100644 index 0000000000000..14d6e52dc0465 --- /dev/null +++ b/src/plugins/kibana_utils/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 { createStateContainer, StateContainer, of } from './common'; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index c634322b23d0b..3d8a4414de70c 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -74,8 +74,10 @@ export { StartSyncStateFnType, StopSyncStateFnType, } from './state_sync'; +export { Configurable, CollectConfigProps } from './ui'; export { removeQueryParam, redirectWhenMissing } from './history'; export { applyDiff } from './state_management/utils/diff_object'; +export { createStartServicesGetter, StartServicesGetter } from './core/create_start_service_getter'; /** dummy plugin, we just want kibanaUtils to have its own bundle */ export function plugin() { diff --git a/src/plugins/kibana_utils/public/ui/configurable.ts b/src/plugins/kibana_utils/public/ui/configurable.ts new file mode 100644 index 0000000000000..a4a9f09c1c0e0 --- /dev/null +++ b/src/plugins/kibana_utils/public/ui/configurable.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 { UiComponent } from '../../common/ui/ui_component'; + +/** + * Represents something that can be configured by user using UI. + */ +export interface Configurable { + /** + * Create default config for this item, used when item is created for the first time. + */ + readonly createConfig: () => Config; + + /** + * Is this config valid. Used to validate user's input before saving. + */ + readonly isConfigValid: (config: Config) => boolean; + + /** + * `UiComponent` to be rendered when collecting configuration for this item. + */ + readonly CollectConfig: UiComponent>; +} + +/** + * Props provided to `CollectConfig` component on every re-render. + */ +export interface CollectConfigProps { + /** + * Current (latest) config of the item. + */ + config: Config; + + /** + * Callback called when user updates the config in UI. + */ + onConfig: (config: Config) => void; + + /** + * Context information about where component is being rendered. + */ + context: Context; +} diff --git a/src/plugins/kibana_utils/public/ui/index.ts b/src/plugins/kibana_utils/public/ui/index.ts new file mode 100644 index 0000000000000..54d47ac7e980f --- /dev/null +++ b/src/plugins/kibana_utils/public/ui/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 * from './configurable'; diff --git a/src/plugins/ui_actions/public/actions/action.ts b/src/plugins/ui_actions/public/actions/action.ts index feaa1f6a60e2f..f5dbbc9f923ac 100644 --- a/src/plugins/ui_actions/public/actions/action.ts +++ b/src/plugins/ui_actions/public/actions/action.ts @@ -19,10 +19,12 @@ import { UiComponent } from 'src/plugins/kibana_utils/public'; import { ActionType, ActionContextMapping } from '../types'; +import { Presentable } from '../util/presentable'; export type ActionByType = Action; -export interface Action { +export interface Action + extends Partial> { /** * Determined the order when there is more than one action matched to a trigger. * Higher numbers are displayed first. @@ -63,14 +65,30 @@ export interface Action { isCompatible(context: Context): Promise; /** - * If this returns something truthy, this will be used as [href] attribute on a link if possible (e.g. in context menu item) - * to support right click -> open in a new tab behavior. - * For regular click navigation is prevented and `execute()` takes control. + * Executes the action. */ - getHref?(context: Context): Promise; + execute(context: Context): Promise; +} + +/** + * A convenience interface used to register an action. + */ +export interface ActionDefinition + extends Partial> { + /** + * ID of the action that uniquely identifies this action in the actions registry. + */ + readonly id: string; + + /** + * ID of the factory for this action. Used to construct dynamic actions. + */ + readonly type?: ActionType; /** * Executes the action. */ execute(context: Context): Promise; } + +export type ActionContext
    = A extends ActionDefinition ? Context : never; diff --git a/src/plugins/ui_actions/public/actions/action_definition.ts b/src/plugins/ui_actions/public/actions/action_definition.ts deleted file mode 100644 index 79fda78401abd..0000000000000 --- a/src/plugins/ui_actions/public/actions/action_definition.ts +++ /dev/null @@ -1,72 +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 { UiComponent } from 'src/plugins/kibana_utils/public'; -import { ActionType, ActionContextMapping } from '../types'; - -export interface ActionDefinition { - /** - * Determined the order when there is more than one action matched to a trigger. - * Higher numbers are displayed first. - */ - order?: number; - - /** - * A unique identifier for this action instance. - */ - id?: string; - - /** - * The action type is what determines the context shape. - */ - readonly type: T; - - /** - * Optional EUI icon type that can be displayed along with the title. - */ - getIconType?(context: ActionContextMapping[T]): string; - - /** - * Returns a title to be displayed to the user. - * @param context - */ - getDisplayName?(context: ActionContextMapping[T]): string; - - /** - * `UiComponent` to render when displaying this action as a context menu item. - * If not provided, `getDisplayName` will be used instead. - */ - MenuItem?: UiComponent<{ context: ActionContextMapping[T] }>; - - /** - * Returns a promise that resolves to true if this action is compatible given the context, - * otherwise resolves to false. - */ - isCompatible?(context: ActionContextMapping[T]): Promise; - - /** - * If this returns something truthy, this is used in addition to the `execute` method when clicked. - */ - getHref?(context: ActionContextMapping[T]): Promise; - - /** - * Executes the action. - */ - execute(context: ActionContextMapping[T]): Promise; -} diff --git a/src/plugins/ui_actions/public/actions/action_internal.test.ts b/src/plugins/ui_actions/public/actions/action_internal.test.ts new file mode 100644 index 0000000000000..b14346180c274 --- /dev/null +++ b/src/plugins/ui_actions/public/actions/action_internal.test.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 { ActionDefinition } from './action'; +import { ActionInternal } from './action_internal'; + +const defaultActionDef: ActionDefinition = { + id: 'test-action', + execute: jest.fn(), +}; + +describe('ActionInternal', () => { + test('can instantiate from action definition', () => { + const action = new ActionInternal(defaultActionDef); + expect(action.id).toBe('test-action'); + }); +}); diff --git a/src/plugins/ui_actions/public/actions/action_internal.ts b/src/plugins/ui_actions/public/actions/action_internal.ts new file mode 100644 index 0000000000000..4cbc4dd2a053c --- /dev/null +++ b/src/plugins/ui_actions/public/actions/action_internal.ts @@ -0,0 +1,58 @@ +/* + * 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 { Action, ActionContext as Context, ActionDefinition } from './action'; +import { Presentable } from '../util/presentable'; +import { uiToReactComponent } from '../../../kibana_react/public'; +import { ActionType } from '../types'; + +export class ActionInternal + implements Action>, Presentable> { + constructor(public readonly definition: A) {} + + public readonly id: string = this.definition.id; + public readonly type: ActionType = this.definition.type || ''; + public readonly order: number = this.definition.order || 0; + public readonly MenuItem? = this.definition.MenuItem; + public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined; + + public execute(context: Context) { + return this.definition.execute(context); + } + + public getIconType(context: Context): string | undefined { + if (!this.definition.getIconType) return undefined; + return this.definition.getIconType(context); + } + + public getDisplayName(context: Context): string { + if (!this.definition.getDisplayName) return `Action: ${this.id}`; + return this.definition.getDisplayName(context); + } + + public async isCompatible(context: Context): Promise { + if (!this.definition.isCompatible) return true; + return await this.definition.isCompatible(context); + } + + public async getHref(context: Context): Promise { + if (!this.definition.getHref) return undefined; + return await this.definition.getHref(context); + } +} diff --git a/src/plugins/ui_actions/public/actions/create_action.ts b/src/plugins/ui_actions/public/actions/create_action.ts index cc66f221e4082..dea21678eccea 100644 --- a/src/plugins/ui_actions/public/actions/create_action.ts +++ b/src/plugins/ui_actions/public/actions/create_action.ts @@ -17,11 +17,19 @@ * under the License. */ +import { ActionContextMapping } from '../types'; import { ActionByType } from './action'; import { ActionType } from '../types'; -import { ActionDefinition } from './action_definition'; +import { ActionDefinition } from './action'; -export function createAction(action: ActionDefinition): ActionByType { +interface ActionDefinitionByType + extends Omit, 'id'> { + id?: string; +} + +export function createAction( + action: ActionDefinitionByType +): ActionByType { return { getIconType: () => undefined, order: 0, @@ -29,5 +37,5 @@ export function createAction(action: ActionDefinition): isCompatible: () => Promise.resolve(true), getDisplayName: () => '', ...action, - }; + } as ActionByType; } diff --git a/src/plugins/ui_actions/public/actions/index.ts b/src/plugins/ui_actions/public/actions/index.ts index 64bfd368e3dfa..88e42ff2ec113 100644 --- a/src/plugins/ui_actions/public/actions/index.ts +++ b/src/plugins/ui_actions/public/actions/index.ts @@ -18,5 +18,6 @@ */ export * from './action'; +export * from './action_internal'; export * from './create_action'; export * from './incompatible_action_error'; diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index d26740ffdf033..0c19d20ed1bda 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -24,19 +24,25 @@ import { i18n } from '@kbn/i18n'; import { uiToReactComponent } from '../../../kibana_react/public'; import { Action } from '../actions'; +export const defaultTitle = i18n.translate('uiActions.actionPanel.title', { + defaultMessage: 'Options', +}); + /** * Transforms an array of Actions to the shape EuiContextMenuPanel expects. */ -export async function buildContextMenuForActions({ +export async function buildContextMenuForActions({ actions, actionContext, + title = defaultTitle, closeMenu, }: { - actions: Array>; - actionContext: A; + actions: Array>; + actionContext: Context; + title?: string; closeMenu: () => void; }): Promise { - const menuItems = await buildEuiContextMenuPanelItems({ + const menuItems = await buildEuiContextMenuPanelItems({ actions, actionContext, closeMenu, @@ -44,9 +50,7 @@ export async function buildContextMenuForActions({ return { id: 'mainMenu', - title: i18n.translate('uiActions.actionPanel.title', { - defaultMessage: 'Options', - }), + title, items: menuItems, }; } @@ -54,49 +58,41 @@ export async function buildContextMenuForActions({ /** * Transform an array of Actions into the shape needed to build an EUIContextMenu */ -async function buildEuiContextMenuPanelItems({ +async function buildEuiContextMenuPanelItems({ actions, actionContext, closeMenu, }: { - actions: Array>; - actionContext: A; + actions: Array>; + actionContext: Context; closeMenu: () => void; }) { - const items: EuiContextMenuPanelItemDescriptor[] = []; - const promises = actions.map(async action => { + const items: EuiContextMenuPanelItemDescriptor[] = new Array(actions.length); + const promises = actions.map(async (action, index) => { const isCompatible = await action.isCompatible(actionContext); if (!isCompatible) { return; } - items.push( - await convertPanelActionToContextMenuItem({ - action, - actionContext, - closeMenu, - }) - ); + items[index] = await convertPanelActionToContextMenuItem({ + action, + actionContext, + closeMenu, + }); }); await Promise.all(promises); - return items; + return items.filter(Boolean); } -/** - * - * @param {ContextMenuAction} action - * @param {Embeddable} embeddable - * @return {Promise} - */ -async function convertPanelActionToContextMenuItem({ +async function convertPanelActionToContextMenuItem({ action, actionContext, closeMenu, }: { - action: Action; - actionContext: A; + action: Action; + actionContext: Context; closeMenu: () => void; }): Promise { const menuPanelItem: EuiContextMenuPanelItemDescriptor = { diff --git a/src/plugins/ui_actions/public/context_menu/open_context_menu.tsx b/src/plugins/ui_actions/public/context_menu/open_context_menu.tsx index 4d794618e85ab..c723388c021e9 100644 --- a/src/plugins/ui_actions/public/context_menu/open_context_menu.tsx +++ b/src/plugins/ui_actions/public/context_menu/open_context_menu.tsx @@ -149,7 +149,11 @@ export function openContextMenu( anchorPosition="downRight" withTitle > - + , container ); diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index 49b6bd5e17699..a9b413fb36542 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -26,8 +26,14 @@ export function plugin(initializerContext: PluginInitializerContext) { export { UiActionsSetup, UiActionsStart } from './plugin'; export { UiActionsServiceParams, UiActionsService } from './service'; -export { Action, createAction, IncompatibleActionError } from './actions'; +export { + Action, + ActionDefinition as UiActionsActionDefinition, + createAction, + IncompatibleActionError, +} from './actions'; export { buildContextMenuForActions } from './context_menu'; +export { Presentable as UiActionsPresentable } from './util'; export { Trigger, TriggerContext, diff --git a/src/plugins/ui_actions/public/mocks.ts b/src/plugins/ui_actions/public/mocks.ts index c1be6b2626525..3522ac4941ba0 100644 --- a/src/plugins/ui_actions/public/mocks.ts +++ b/src/plugins/ui_actions/public/mocks.ts @@ -28,10 +28,12 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { + addTriggerAction: jest.fn(), attachAction: jest.fn(), detachAction: jest.fn(), registerAction: jest.fn(), registerTrigger: jest.fn(), + unregisterAction: jest.fn(), }; return setupContract; }; @@ -39,16 +41,18 @@ const createSetupContract = (): Setup => { const createStartContract = (): Start => { const startContract: Start = { attachAction: jest.fn(), - registerAction: jest.fn(), - registerTrigger: jest.fn(), - getAction: jest.fn(), + unregisterAction: jest.fn(), + addTriggerAction: jest.fn(), + clear: jest.fn(), detachAction: jest.fn(), executeTriggerActions: jest.fn(), + fork: jest.fn(), + getAction: jest.fn(), getTrigger: jest.fn(), getTriggerActions: jest.fn((id: TriggerId) => []), getTriggerCompatibleActions: jest.fn(), - clear: jest.fn(), - fork: jest.fn(), + registerAction: jest.fn(), + registerTrigger: jest.fn(), }; return startContract; diff --git a/src/plugins/ui_actions/public/plugin.ts b/src/plugins/ui_actions/public/plugin.ts index 928e57937a9b5..71148656cbb16 100644 --- a/src/plugins/ui_actions/public/plugin.ts +++ b/src/plugins/ui_actions/public/plugin.ts @@ -23,7 +23,12 @@ import { selectRangeTrigger, valueClickTrigger, applyFilterTrigger } from './tri export type UiActionsSetup = Pick< UiActionsService, - 'attachAction' | 'detachAction' | 'registerAction' | 'registerTrigger' + | 'addTriggerAction' + | 'attachAction' + | 'detachAction' + | 'registerAction' + | 'registerTrigger' + | 'unregisterAction' >; export type UiActionsStart = PublicMethodsOf; diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts index bdf71a25e6dbc..45a1bdffa52ad 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts @@ -18,7 +18,7 @@ */ import { UiActionsService } from './ui_actions_service'; -import { Action, createAction } from '../actions'; +import { Action, ActionInternal, createAction } from '../actions'; import { createHelloWorldAction } from '../tests/test_samples'; import { ActionRegistry, TriggerRegistry, TriggerId, ActionType } from '../types'; import { Trigger } from '../triggers'; @@ -102,6 +102,21 @@ describe('UiActionsService', () => { type: 'test' as ActionType, }); }); + + test('return action instance', () => { + const service = new UiActionsService(); + const action = service.registerAction({ + id: 'test', + execute: async () => {}, + getDisplayName: () => 'test', + getIconType: () => '', + isCompatible: async () => true, + type: 'test' as ActionType, + }); + + expect(action).toBeInstanceOf(ActionInternal); + expect(action.id).toBe('test'); + }); }); describe('.getTriggerActions()', () => { @@ -139,13 +154,14 @@ describe('UiActionsService', () => { expect(list0).toHaveLength(0); - service.attachAction(FOO_TRIGGER, action1); + service.addTriggerAction(FOO_TRIGGER, action1); const list1 = service.getTriggerActions(FOO_TRIGGER); expect(list1).toHaveLength(1); - expect(list1).toEqual([action1]); + expect(list1[0]).toBeInstanceOf(ActionInternal); + expect(list1[0].id).toBe(action1.id); - service.attachAction(FOO_TRIGGER, action2); + service.addTriggerAction(FOO_TRIGGER, action2); const list2 = service.getTriggerActions(FOO_TRIGGER); expect(list2).toHaveLength(2); @@ -164,7 +180,7 @@ describe('UiActionsService', () => { service.registerAction(helloWorldAction); expect(actions.size - length).toBe(1); - expect(actions.get(helloWorldAction.id)).toBe(helloWorldAction); + expect(actions.get(helloWorldAction.id)!.id).toBe(helloWorldAction.id); }); test('getTriggerCompatibleActions returns attached actions', async () => { @@ -178,7 +194,7 @@ describe('UiActionsService', () => { title: 'My trigger', }; service.registerTrigger(testTrigger); - service.attachAction(MY_TRIGGER, helloWorldAction); + service.addTriggerAction(MY_TRIGGER, helloWorldAction); const compatibleActions = await service.getTriggerCompatibleActions(MY_TRIGGER, { hi: 'there', @@ -204,7 +220,7 @@ describe('UiActionsService', () => { }; service.registerTrigger(testTrigger); - service.attachAction(testTrigger.id, action); + service.addTriggerAction(testTrigger.id, action); const compatibleActions1 = await service.getTriggerCompatibleActions(testTrigger.id, { accept: true, @@ -288,7 +304,7 @@ describe('UiActionsService', () => { id: FOO_TRIGGER, }); service1.registerAction(testAction1); - service1.attachAction(FOO_TRIGGER, testAction1); + service1.addTriggerAction(FOO_TRIGGER, testAction1); const service2 = service1.fork(); @@ -309,14 +325,14 @@ describe('UiActionsService', () => { }); service1.registerAction(testAction1); service1.registerAction(testAction2); - service1.attachAction(FOO_TRIGGER, testAction1); + service1.addTriggerAction(FOO_TRIGGER, testAction1); const service2 = service1.fork(); expect(service1.getTriggerActions(FOO_TRIGGER)).toHaveLength(1); expect(service2.getTriggerActions(FOO_TRIGGER)).toHaveLength(1); - service2.attachAction(FOO_TRIGGER, testAction2); + service2.addTriggerAction(FOO_TRIGGER, testAction2); expect(service1.getTriggerActions(FOO_TRIGGER)).toHaveLength(1); expect(service2.getTriggerActions(FOO_TRIGGER)).toHaveLength(2); @@ -330,14 +346,14 @@ describe('UiActionsService', () => { }); service1.registerAction(testAction1); service1.registerAction(testAction2); - service1.attachAction(FOO_TRIGGER, testAction1); + service1.addTriggerAction(FOO_TRIGGER, testAction1); const service2 = service1.fork(); expect(service1.getTriggerActions(FOO_TRIGGER)).toHaveLength(1); expect(service2.getTriggerActions(FOO_TRIGGER)).toHaveLength(1); - service1.attachAction(FOO_TRIGGER, testAction2); + service1.addTriggerAction(FOO_TRIGGER, testAction2); expect(service1.getTriggerActions(FOO_TRIGGER)).toHaveLength(2); expect(service2.getTriggerActions(FOO_TRIGGER)).toHaveLength(1); @@ -392,7 +408,7 @@ describe('UiActionsService', () => { } as any; service.registerTrigger(trigger); - service.attachAction(MY_TRIGGER, action); + service.addTriggerAction(MY_TRIGGER, action); const actions = service.getTriggerActions(trigger.id); @@ -400,7 +416,7 @@ describe('UiActionsService', () => { expect(actions[0].id).toBe(ACTION_HELLO_WORLD); }); - test('can detach an action to a trigger', () => { + test('can detach an action from a trigger', () => { const service = new UiActionsService(); const trigger: Trigger = { @@ -413,7 +429,7 @@ describe('UiActionsService', () => { service.registerTrigger(trigger); service.registerAction(action); - service.attachAction(trigger.id, action); + service.addTriggerAction(trigger.id, action); service.detachAction(trigger.id, action.id); const actions2 = service.getTriggerActions(trigger.id); @@ -445,7 +461,7 @@ describe('UiActionsService', () => { } as any; service.registerAction(action); - expect(() => service.attachAction('i do not exist' as TriggerId, action)).toThrowError( + expect(() => service.addTriggerAction('i do not exist' as TriggerId, action)).toThrowError( 'No trigger [triggerId = i do not exist] exists, for attaching action [actionId = ACTION_HELLO_WORLD].' ); }); diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index f7718e63773f5..9a08aeabb00f3 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -23,9 +23,8 @@ import { TriggerToActionsRegistry, TriggerId, TriggerContextMapping, - ActionType, } from '../types'; -import { Action, ActionByType } from '../actions'; +import { ActionInternal, Action, ActionDefinition, ActionContext } from '../actions'; import { Trigger, TriggerContext } from '../triggers/trigger'; import { TriggerInternal } from '../triggers/trigger_internal'; import { TriggerContract } from '../triggers/trigger_contract'; @@ -76,49 +75,41 @@ export class UiActionsService { return trigger.contract; }; - public readonly registerAction = (action: ActionByType) => { - if (this.actions.has(action.id)) { - throw new Error(`Action [action.id = ${action.id}] already registered.`); + public readonly registerAction = ( + definition: A + ): Action> => { + if (this.actions.has(definition.id)) { + throw new Error(`Action [action.id = ${definition.id}] already registered.`); } + const action = new ActionInternal(definition); + this.actions.set(action.id, action); + + return action; }; - public readonly getAction = (id: string): ActionByType => { - if (!this.actions.has(id)) { - throw new Error(`Action [action.id = ${id}] not registered.`); + public readonly unregisterAction = (actionId: string): void => { + if (!this.actions.has(actionId)) { + throw new Error(`Action [action.id = ${actionId}] is not registered.`); } - return this.actions.get(id) as ActionByType; + this.actions.delete(actionId); }; - public readonly attachAction = ( - triggerId: TType, - // The action can accept partial or no context, but if it needs context not provided - // by this type of trigger, typescript will complain. yay! - action: ActionByType & Action - ): void => { - if (!this.actions.has(action.id)) { - this.registerAction(action); - } else { - const registeredAction = this.actions.get(action.id); - if (registeredAction !== action) { - throw new Error(`A different action instance with this id is already registered.`); - } - } - + public readonly attachAction = (triggerId: T, actionId: string): void => { const trigger = this.triggers.get(triggerId); if (!trigger) { throw new Error( - `No trigger [triggerId = ${triggerId}] exists, for attaching action [actionId = ${action.id}].` + `No trigger [triggerId = ${triggerId}] exists, for attaching action [actionId = ${actionId}].` ); } const actionIds = this.triggerToActions.get(triggerId); - if (!actionIds!.find(id => id === action.id)) { - this.triggerToActions.set(triggerId, [...actionIds!, action.id]); + if (!actionIds!.find(id => id === actionId)) { + this.triggerToActions.set(triggerId, [...actionIds!, actionId]); } }; @@ -139,6 +130,32 @@ export class UiActionsService { ); }; + /** + * `addTriggerAction` is similar to `attachAction` as it attaches action to a + * trigger, but it also registers the action, if it has not been registered, yet. + * + * `addTriggerAction` also infers better typing of the `action` argument. + */ + public readonly addTriggerAction = ( + triggerId: T, + // The action can accept partial or no context, but if it needs context not provided + // by this type of trigger, typescript will complain. yay! + action: Action + ): void => { + if (!this.actions.has(action.id)) this.registerAction(action); + this.attachAction(triggerId, action.id); + }; + + public readonly getAction = ( + id: string + ): Action> => { + if (!this.actions.has(id)) { + throw new Error(`Action [action.id = ${id}] not registered.`); + } + + return this.actions.get(id) as ActionInternal; + }; + public readonly getTriggerActions = ( triggerId: T ): Array> => { @@ -147,9 +164,9 @@ export class UiActionsService { const actionIds = this.triggerToActions.get(triggerId); - const actions = actionIds!.map(actionId => this.actions.get(actionId)).filter(Boolean) as Array< - Action - >; + const actions = actionIds! + .map(actionId => this.actions.get(actionId) as ActionInternal) + .filter(Boolean); return actions as Array>>; }; diff --git a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts index 5b427f918c173..ade21ee4b7d91 100644 --- a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts +++ b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts @@ -69,7 +69,7 @@ test('executes a single action mapped to a trigger', async () => { const action = createTestAction('test1', () => true); setup.registerTrigger(trigger); - setup.attachAction(trigger.id, action); + setup.addTriggerAction(trigger.id, action); const context = {}; const start = doStart(); @@ -109,7 +109,7 @@ test('does not execute an incompatible action', async () => { ); setup.registerTrigger(trigger); - setup.attachAction(trigger.id, action); + setup.addTriggerAction(trigger.id, action); const start = doStart(); const context = { @@ -130,8 +130,8 @@ test('shows a context menu when more than one action is mapped to a trigger', as const action2 = createTestAction('test2', () => true); setup.registerTrigger(trigger); - setup.attachAction(trigger.id, action1); - setup.attachAction(trigger.id, action2); + setup.addTriggerAction(trigger.id, action1); + setup.addTriggerAction(trigger.id, action2); expect(openContextMenu).toHaveBeenCalledTimes(0); @@ -155,7 +155,7 @@ test('passes whole action context to isCompatible()', async () => { }); setup.registerTrigger(trigger); - setup.attachAction(trigger.id, action); + setup.addTriggerAction(trigger.id, action); const start = doStart(); diff --git a/src/plugins/ui_actions/public/tests/get_trigger_actions.test.ts b/src/plugins/ui_actions/public/tests/get_trigger_actions.test.ts index f5a6a96fb41a4..55ccac42ff255 100644 --- a/src/plugins/ui_actions/public/tests/get_trigger_actions.test.ts +++ b/src/plugins/ui_actions/public/tests/get_trigger_actions.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Action } from '../actions'; +import { ActionInternal, Action } from '../actions'; import { uiActionsPluginMock } from '../mocks'; import { TriggerId, ActionType } from '../types'; @@ -47,13 +47,14 @@ test('returns actions set on trigger', () => { expect(list0).toHaveLength(0); - setup.attachAction('trigger' as TriggerId, action1); + setup.addTriggerAction('trigger' as TriggerId, action1); const list1 = start.getTriggerActions('trigger' as TriggerId); expect(list1).toHaveLength(1); - expect(list1).toEqual([action1]); + expect(list1[0]).toBeInstanceOf(ActionInternal); + expect(list1[0].id).toBe(action1.id); - setup.attachAction('trigger' as TriggerId, action2); + setup.addTriggerAction('trigger' as TriggerId, action2); const list2 = start.getTriggerActions('trigger' as TriggerId); expect(list2).toHaveLength(2); diff --git a/src/plugins/ui_actions/public/tests/get_trigger_compatible_actions.test.ts b/src/plugins/ui_actions/public/tests/get_trigger_compatible_actions.test.ts index c5e68e5d5ca5a..21dd17ed82e3f 100644 --- a/src/plugins/ui_actions/public/tests/get_trigger_compatible_actions.test.ts +++ b/src/plugins/ui_actions/public/tests/get_trigger_compatible_actions.test.ts @@ -37,7 +37,7 @@ beforeEach(() => { id: 'trigger' as TriggerId, title: 'trigger', }); - uiActions.setup.attachAction('trigger' as TriggerId, action); + uiActions.setup.addTriggerAction('trigger' as TriggerId, action); }); test('can register action', async () => { @@ -58,7 +58,7 @@ test('getTriggerCompatibleActions returns attached actions', async () => { title: 'My trigger', }; setup.registerTrigger(testTrigger); - setup.attachAction('MY-TRIGGER' as TriggerId, helloWorldAction); + setup.addTriggerAction('MY-TRIGGER' as TriggerId, helloWorldAction); const start = doStart(); const actions = await start.getTriggerCompatibleActions('MY-TRIGGER' as TriggerId, {}); @@ -84,7 +84,7 @@ test('filters out actions not applicable based on the context', async () => { setup.registerTrigger(testTrigger); setup.registerAction(action1); - setup.attachAction(testTrigger.id, action1); + setup.addTriggerAction(testTrigger.id, action1); const start = doStart(); let actions = await start.getTriggerCompatibleActions(testTrigger.id, { accept: true }); diff --git a/src/plugins/ui_actions/public/tests/test_samples/index.ts b/src/plugins/ui_actions/public/tests/test_samples/index.ts index 7d63b1b6d5669..dfa71cec89595 100644 --- a/src/plugins/ui_actions/public/tests/test_samples/index.ts +++ b/src/plugins/ui_actions/public/tests/test_samples/index.ts @@ -16,4 +16,5 @@ * specific language governing permissions and limitations * under the License. */ + export { createHelloWorldAction } from './hello_world_action'; diff --git a/src/plugins/ui_actions/public/triggers/select_range_trigger.ts b/src/plugins/ui_actions/public/triggers/select_range_trigger.ts index c638db0ce9dab..c7c998907381a 100644 --- a/src/plugins/ui_actions/public/triggers/select_range_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/select_range_trigger.ts @@ -22,6 +22,8 @@ import { Trigger } from '.'; export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER'; export const selectRangeTrigger: Trigger<'SELECT_RANGE_TRIGGER'> = { id: SELECT_RANGE_TRIGGER, - title: 'Select range', + // This is empty string to hide title of ui_actions context menu that appears + // when this trigger is executed. + title: '', description: 'Applies a range filter', }; diff --git a/src/plugins/ui_actions/public/triggers/trigger_internal.ts b/src/plugins/ui_actions/public/triggers/trigger_internal.ts index 1fc92d7c0cb1b..e499c404ae745 100644 --- a/src/plugins/ui_actions/public/triggers/trigger_internal.ts +++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts @@ -65,8 +65,11 @@ export class TriggerInternal { const panel = await buildContextMenuForActions({ actions, actionContext: context, + title: this.trigger.title, closeMenu: () => session.close(), }); - const session = openContextMenu([panel]); + const session = openContextMenu([panel], { + 'data-test-subj': 'multipleActionsContextMenu', + }); } } diff --git a/src/plugins/ui_actions/public/triggers/value_click_trigger.ts b/src/plugins/ui_actions/public/triggers/value_click_trigger.ts index ad32bdc1b564e..5fe060f55dc77 100644 --- a/src/plugins/ui_actions/public/triggers/value_click_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/value_click_trigger.ts @@ -22,6 +22,8 @@ import { Trigger } from '.'; export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER'; export const valueClickTrigger: Trigger<'VALUE_CLICK_TRIGGER'> = { id: VALUE_CLICK_TRIGGER, - title: 'Value clicked', + // This is empty string to hide title of ui_actions context menu that appears + // when this trigger is executed. + title: '', description: 'Value was clicked', }; diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index e6247a8bafff7..85c87306cc4f9 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { ActionByType } from './actions/action'; +import { ActionInternal } from './actions/action_internal'; import { TriggerInternal } from './triggers/trigger_internal'; import { Filter } from '../../data/public'; import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers'; @@ -25,7 +25,7 @@ import { IEmbeddable } from '../../embeddable/public'; import { RangeSelectTriggerContext, ValueClickTriggerContext } from '../../embeddable/public'; export type TriggerRegistry = Map>; -export type ActionRegistry = Map>; +export type ActionRegistry = Map; export type TriggerToActionsRegistry = Map; const DEFAULT_TRIGGER = ''; diff --git a/src/plugins/ui_actions/public/util/index.ts b/src/plugins/ui_actions/public/util/index.ts new file mode 100644 index 0000000000000..a6943e54f016c --- /dev/null +++ b/src/plugins/ui_actions/public/util/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 * from './presentable'; diff --git a/src/plugins/ui_actions/public/util/presentable.ts b/src/plugins/ui_actions/public/util/presentable.ts new file mode 100644 index 0000000000000..f43b776e74658 --- /dev/null +++ b/src/plugins/ui_actions/public/util/presentable.ts @@ -0,0 +1,65 @@ +/* + * 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 { UiComponent } from 'src/plugins/kibana_utils/public'; + +/** + * Represents something that can be displayed to user in UI. + */ +export interface Presentable { + /** + * ID that uniquely identifies this object. + */ + readonly id: string; + + /** + * Determines the display order in relation to other items. Higher numbers are + * displayed first. + */ + readonly order: number; + + /** + * `UiComponent` to render when displaying this entity as a context menu item. + * If not provided, `getDisplayName` will be used instead. + */ + readonly MenuItem?: UiComponent<{ context: Context }>; + + /** + * Optional EUI icon type that can be displayed along with the title. + */ + getIconType(context: Context): string | undefined; + + /** + * Returns a title to be displayed to the user. + */ + getDisplayName(context: Context): string; + + /** + * This method should return a link if this item can be clicked on. The link + * is used to navigate user if user middle-clicks it or Ctrl + clicks or + * right-clicks and selects "Open in new tab". + */ + getHref?(context: Context): Promise; + + /** + * Returns a promise that resolves to true if this item is compatible given + * the context and should be displayed to user, otherwise resolves to false. + */ + isCompatible(context: Context): Promise; +} diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 1c545bb36cff0..71b31b7f74168 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -265,6 +265,7 @@ export class VisualizeEmbeddable extends Embeddable} @@ -512,6 +517,20 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide return checkList.filter(viz => viz.isPresent === false).map(viz => viz.name); } + + public async getPanelDrilldownCount(panelIndex = 0): Promise { + log.debug('getPanelDrilldownCount'); + const panel = (await this.getDashboardPanels())[panelIndex]; + try { + const count = await panel.findByTestSubject( + 'embeddablePanelNotification-ACTION_PANEL_NOTIFICATIONS' + ); + return Number.parseInt(await count.getVisibleText(), 10); + } catch (e) { + // if not found then this is 0 (we don't show badge with 0) + return 0; + } + } } return new DashboardPage(); diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/public/plugin.ts b/test/plugin_functional/plugins/kbn_sample_panel_action/public/plugin.ts index 8ea8d2ff49e3b..9ae1021227315 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/public/plugin.ts +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/public/plugin.ts @@ -27,14 +27,10 @@ export class SampelPanelActionTestPlugin implements Plugin { public setup(core: CoreSetup, { uiActions }: { uiActions: UiActionsSetup }) { const samplePanelAction = createSamplePanelAction(core.getStartServices); - - uiActions.registerAction(samplePanelAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, samplePanelAction); - const samplePanelLink = createSamplePanelLink(); - uiActions.registerAction(samplePanelLink); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, samplePanelLink); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, samplePanelAction); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, samplePanelLink); return {}; } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx index e5f5faa6ac361..b47e84216dd16 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx @@ -69,11 +69,10 @@ export class EmbeddableExplorerPublicPlugin const sayHelloAction = new SayHelloAction(alert); const sendMessageAction = createSendMessageAction(core.overlays); - plugins.uiActions.registerAction(helloWorldAction); plugins.uiActions.registerAction(sayHelloAction); plugins.uiActions.registerAction(sendMessageAction); - plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, helloWorldAction); + plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, helloWorldAction); plugins.__LEGACY.onRenderComplete(() => { const root = document.getElementById(REACT_ROOT_ID); diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 9f43bf8da0601..ccf8739dd9730 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -3,11 +3,13 @@ "paths": { "xpack.actions": "plugins/actions", "xpack.advancedUiActions": "plugins/advanced_ui_actions", + "xpack.uiActionsEnhanced": "examples/ui_actions_enhanced_examples", "xpack.alerting": "plugins/alerting", "xpack.alertingBuiltins": "plugins/alerting_builtins", "xpack.apm": ["legacy/plugins/apm", "plugins/apm"], "xpack.beatsManagement": "legacy/plugins/beats_management", "xpack.canvas": "legacy/plugins/canvas", + "xpack.dashboard": "plugins/dashboard_enhanced", "xpack.crossClusterReplication": "plugins/cross_cluster_replication", "xpack.dashboardMode": "legacy/plugins/dashboard_mode", "xpack.data": "plugins/data_enhanced", diff --git a/x-pack/examples/ui_actions_enhanced_examples/README.md b/x-pack/examples/ui_actions_enhanced_examples/README.md index c9f53137d8687..ec049bbd33dec 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/README.md +++ b/x-pack/examples/ui_actions_enhanced_examples/README.md @@ -1,3 +1,36 @@ -## Ui actions enhanced examples +# Ui actions enhanced examples -To run this example, use the command `yarn start --run-examples`. +To run this example plugin, use the command `yarn start --run-examples`. + + +## Drilldown examples + +This plugin holds few examples on how to add drilldown types to dashboard. + +To play with drilldowns, open any dashboard, click "Edit" to put it in *edit mode*. +Now when opening context menu of dashboard panels you should see "Create drilldown" option. + +![image](https://user-images.githubusercontent.com/9773803/80460907-c2ef7880-8934-11ea-8400-533bb9d57e36.png) + +Once you click "Create drilldown" you should be able to see drilldowns added by +this sample plugin. + +![image](https://user-images.githubusercontent.com/9773803/80460408-131a0b00-8934-11ea-81e4-137e9e33f34b.png) + + +### `dashboard_hello_world_drilldown` + +`dashboard_hello_world_drilldown` is the most basic "hello world" example showing +how a drilldown can be built, all in one file. + +### `dashboard_to_url_drilldown` + +`dashboard_to_url_drilldown` is a good starting point for build a drilldown +that navigates somewhere externally. + +One can see how middle-click or Ctrl + click behavior could be supported using +`getHref` field. + +### `dashboard_to_discover_drilldown` + +`dashboard_to_discover_drilldown` shows how a real-world drilldown could look like. diff --git a/x-pack/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json index f75852edced5c..e220cdd5cd297 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/kibana.json +++ b/x-pack/examples/ui_actions_enhanced_examples/kibana.json @@ -5,6 +5,6 @@ "configPath": ["ui_actions_enhanced_examples"], "server": false, "ui": true, - "requiredPlugins": ["uiActions", "data"], + "requiredPlugins": ["advancedUiActions", "data"], "optionalPlugins": [] } diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/README.md b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/README.md new file mode 100644 index 0000000000000..47a3429b16d7a --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/README.md @@ -0,0 +1 @@ +This folder contains a one-file example of the most basic drilldown implementation. diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx new file mode 100644 index 0000000000000..b1e1040daee6e --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx @@ -0,0 +1,60 @@ +/* + * 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 { EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; +import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/advanced_ui_actions/public'; +import { + RangeSelectTriggerContext, + ValueClickTriggerContext, +} from '../../../../../src/plugins/embeddable/public'; +import { CollectConfigProps } from '../../../../../src/plugins/kibana_utils/public'; + +export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext; + +export interface Config { + name: string; +} + +const SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN = 'SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN'; + +export class DashboardHelloWorldDrilldown implements Drilldown { + public readonly id = SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN; + + public readonly order = 6; + + public readonly getDisplayName = () => 'Say hello drilldown'; + + public readonly euiIcon = 'cheer'; + + private readonly ReactCollectConfig: React.FC> = ({ + config, + onConfig, + }) => ( + + onConfig({ ...config, name: event.target.value })} + /> + + ); + + public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); + + public readonly createConfig = () => ({ + name: '', + }); + + public readonly isConfigValid = (config: Config): config is Config => { + return !!config.name; + }; + + public readonly execute = async (config: Config, context: ActionContext) => { + alert(`Hello, ${config.name}`); + }; +} diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/collect_config_container.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/collect_config_container.tsx new file mode 100644 index 0000000000000..69cf260a20a81 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/collect_config_container.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, { useState, useEffect } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; +import { CollectConfigProps } from './types'; +import { DiscoverDrilldownConfig, IndexPatternItem } from './components/discover_drilldown_config'; +import { Params } from './drilldown'; + +export interface CollectConfigContainerProps extends CollectConfigProps { + params: Params; +} + +export const CollectConfigContainer: React.FC = ({ + config, + onConfig, + params: { start }, +}) => { + const isMounted = useMountedState(); + const [indexPatterns, setIndexPatterns] = useState([]); + + useEffect(() => { + (async () => { + const indexPatternSavedObjects = await start().plugins.data.indexPatterns.getCache(); + if (!isMounted()) return; + setIndexPatterns( + indexPatternSavedObjects + ? indexPatternSavedObjects.map(indexPattern => ({ + id: indexPattern.id, + title: indexPattern.attributes.title, + })) + : [] + ); + })(); + }, [isMounted, start]); + + return ( + { + onConfig({ ...config, indexPatternId }); + }} + customIndexPattern={config.customIndexPattern} + onCustomIndexPatternToggle={() => + onConfig({ + ...config, + customIndexPattern: !config.customIndexPattern, + indexPatternId: undefined, + }) + } + carryFiltersAndQuery={config.carryFiltersAndQuery} + onCarryFiltersAndQueryToggle={() => + onConfig({ + ...config, + carryFiltersAndQuery: !config.carryFiltersAndQuery, + }) + } + carryTimeRange={config.carryTimeRange} + onCarryTimeRangeToggle={() => + onConfig({ + ...config, + carryTimeRange: !config.carryTimeRange, + }) + } + /> + ); +}; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/discover_drilldown_config.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/discover_drilldown_config.tsx new file mode 100644 index 0000000000000..cf379b29a0039 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/discover_drilldown_config.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFormRow, EuiSelect, EuiSwitch, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { txtChooseDestinationIndexPattern } from './i18n'; + +export interface IndexPatternItem { + id: string; + title: string; +} + +export interface DiscoverDrilldownConfigProps { + activeIndexPatternId?: string; + indexPatterns: IndexPatternItem[]; + onIndexPatternSelect: (indexPatternId: string) => void; + customIndexPattern?: boolean; + onCustomIndexPatternToggle?: () => void; + carryFiltersAndQuery?: boolean; + onCarryFiltersAndQueryToggle?: () => void; + carryTimeRange?: boolean; + onCarryTimeRangeToggle?: () => void; +} + +export const DiscoverDrilldownConfig: React.FC = ({ + activeIndexPatternId, + indexPatterns, + onIndexPatternSelect, + customIndexPattern, + onCustomIndexPatternToggle, + carryFiltersAndQuery, + onCarryFiltersAndQueryToggle, + carryTimeRange, + onCarryTimeRangeToggle, +}) => { + return ( + <> + +

    + This is an example drilldown. It is meant as a starting point for developers, so they can + grab this code and get started. It does not provide a complete working functionality but + serves as a getting started example. +

    +

    + Implementation of the actual Go to Discover drilldown is tracked in{' '} + #60227 +

    + + + {!!onCustomIndexPatternToggle && ( + <> + + + + {!!customIndexPattern && ( + + ({ value: id, text: title })), + ]} + value={activeIndexPatternId || ''} + onChange={e => onIndexPatternSelect(e.target.value)} + /> + + )} + + + )} + + {!!onCarryFiltersAndQueryToggle && ( + + + + )} + {!!onCarryTimeRangeToggle && ( + + + + )} + + ); +}; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/i18n.ts b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/i18n.ts new file mode 100644 index 0000000000000..ccd75e7dcc3e3 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/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 txtChooseDestinationIndexPattern = i18n.translate( + 'xpack.uiActionsEnhanced.components.DiscoverDrilldownConfig.chooseIndexPattern', + { + defaultMessage: 'Choose destination index pattern', + } +); diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/index.ts b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/index.ts new file mode 100644 index 0000000000000..b975a73e55621 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/discover_drilldown_config/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './discover_drilldown_config'; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/index.ts b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/index.ts new file mode 100644 index 0000000000000..b975a73e55621 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './discover_drilldown_config'; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/constants.ts b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/constants.ts new file mode 100644 index 0000000000000..518642866c2b5 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/constants.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN = 'SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN'; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx new file mode 100644 index 0000000000000..1213ec2f35995 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx @@ -0,0 +1,82 @@ +/* + * 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 { StartDependencies as Start } from '../plugin'; +import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; +import { StartServicesGetter } from '../../../../../src/plugins/kibana_utils/public'; +import { ActionContext, Config, CollectConfigProps } from './types'; +import { CollectConfigContainer } from './collect_config_container'; +import { SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN } from './constants'; +import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/advanced_ui_actions/public'; +import { txtGoToDiscover } from './i18n'; + +const isOutputWithIndexPatterns = ( + output: unknown +): output is { indexPatterns: Array<{ id: string }> } => { + if (!output || typeof output !== 'object') return false; + return Array.isArray((output as any).indexPatterns); +}; + +export interface Params { + start: StartServicesGetter>; +} + +export class DashboardToDiscoverDrilldown implements Drilldown { + constructor(protected readonly params: Params) {} + + public readonly id = SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN; + + public readonly order = 10; + + public readonly getDisplayName = () => txtGoToDiscover; + + public readonly euiIcon = 'discoverApp'; + + private readonly ReactCollectConfig: React.FC = props => ( + + ); + + public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); + + public readonly createConfig = () => ({ + customIndexPattern: false, + carryFiltersAndQuery: true, + carryTimeRange: true, + }); + + public readonly isConfigValid = (config: Config): config is Config => { + if (config.customIndexPattern && !config.indexPatternId) return false; + return true; + }; + + private readonly getPath = async (config: Config, context: ActionContext): Promise => { + let indexPatternId = + !!config.customIndexPattern && !!config.indexPatternId ? config.indexPatternId : ''; + + if (!indexPatternId && !!context.embeddable) { + const output = context.embeddable!.getOutput(); + if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) { + indexPatternId = output.indexPatterns[0].id; + } + } + + const index = indexPatternId ? `,index:'${indexPatternId}'` : ''; + return `#/discover?_g=(filters:!(),refreshInterval:(pause:!f,value:900000),time:(from:now-7d,to:now))&_a=(columns:!(_source),filters:!()${index},interval:auto,query:(language:kuery,query:''),sort:!())`; + }; + + public readonly getHref = async (config: Config, context: ActionContext): Promise => { + return `kibana${await this.getPath(config, context)}`; + }; + + public readonly execute = async (config: Config, context: ActionContext) => { + const path = await this.getPath(config, context); + + await this.params.start().core.application.navigateToApp('kibana', { + path, + }); + }; +} diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/i18n.ts b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/i18n.ts new file mode 100644 index 0000000000000..3e92a9f3f1fe4 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/i18n.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtGoToDiscover = i18n.translate('xpack.uiActionsEnhanced.drilldown.goToDiscover', { + defaultMessage: 'Go to Discover (example)', +}); diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/index.ts b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/index.ts new file mode 100644 index 0000000000000..e824c49a6f1fa --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN } from './constants'; +export { + DashboardToDiscoverDrilldown, + Params as DashboardToDiscoverDrilldownParams, +} from './drilldown'; +export { + ActionContext as DashboardToDiscoverActionContext, + Config as DashboardToDiscoverConfig, +} from './types'; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts new file mode 100644 index 0000000000000..5dfc250a56d28 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + RangeSelectTriggerContext, + ValueClickTriggerContext, +} from '../../../../../src/plugins/embeddable/public'; +import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public'; + +export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext; + +export interface Config { + /** + * Whether to use a user selected index pattern, stored in `indexPatternId` field. + */ + customIndexPattern: boolean; + + /** + * ID of index pattern picked by user in UI. If not set, drilldown will use + * the index pattern of the visualization. + */ + indexPatternId?: string; + + /** + * Whether to carry over source dashboard filters and query. + */ + carryFiltersAndQuery: boolean; + + /** + * Whether to carry over source dashboard time range. + */ + carryTimeRange: boolean; +} + +export type CollectConfigProps = CollectConfigPropsBase; diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx new file mode 100644 index 0000000000000..cc38386b26385 --- /dev/null +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx @@ -0,0 +1,114 @@ +/* + * 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 { EuiFormRow, EuiSwitch, EuiFieldText, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; +import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/advanced_ui_actions/public'; +import { + RangeSelectTriggerContext, + ValueClickTriggerContext, +} from '../../../../../src/plugins/embeddable/public'; +import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public'; + +function isValidUrl(url: string) { + try { + new URL(url); + return true; + } catch { + return false; + } +} + +export type ActionContext = RangeSelectTriggerContext | ValueClickTriggerContext; + +export interface Config { + url: string; + openInNewTab: boolean; +} + +export type CollectConfigProps = CollectConfigPropsBase; + +const SAMPLE_DASHBOARD_TO_URL_DRILLDOWN = 'SAMPLE_DASHBOARD_TO_URL_DRILLDOWN'; + +export class DashboardToUrlDrilldown implements Drilldown { + public readonly id = SAMPLE_DASHBOARD_TO_URL_DRILLDOWN; + + public readonly order = 8; + + public readonly getDisplayName = () => 'Go to URL (example)'; + + public readonly euiIcon = 'link'; + + private readonly ReactCollectConfig: React.FC = ({ config, onConfig }) => ( + <> + +

    + This is an example drilldown. It is meant as a starting point for developers, so they can + grab this code and get started. It does not provide a complete working functionality but + serves as a getting started example. +

    +

    + Implementation of the actual Go to URL drilldown is tracked in{' '} + #55324 +

    +
    + + + onConfig({ ...config, url: event.target.value })} + onBlur={() => { + if (!config.url) return; + if (/https?:\/\//.test(config.url)) return; + onConfig({ ...config, url: 'https://' + config.url }); + }} + /> + + + onConfig({ ...config, openInNewTab: !config.openInNewTab })} + /> + + + ); + + public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); + + public readonly createConfig = () => ({ + url: '', + openInNewTab: false, + }); + + public readonly isConfigValid = (config: Config): config is Config => { + if (!config.url) return false; + return isValidUrl(config.url); + }; + + /** + * `getHref` is need to support mouse middle-click and Cmd + Click behavior + * to open a link in new tab. + */ + public readonly getHref = async (config: Config, context: ActionContext) => { + return config.url; + }; + + public readonly execute = async (config: Config, context: ActionContext) => { + const url = await this.getHref(config, context); + + if (config.openInNewTab) { + window.open(url, '_blank'); + } else { + window.location.href = url; + } + }; +} diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts index a4c43753c8247..0d4f274caf57f 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts @@ -5,24 +5,37 @@ */ import { Plugin, CoreSetup, CoreStart } from '../../../../src/core/public'; -import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { + AdvancedUiActionsSetup, + AdvancedUiActionsStart, +} from '../../../../x-pack/plugins/advanced_ui_actions/public'; +import { DashboardHelloWorldDrilldown } from './dashboard_hello_world_drilldown'; +import { DashboardToUrlDrilldown } from './dashboard_to_url_drilldown'; +import { DashboardToDiscoverDrilldown } from './dashboard_to_discover_drilldown'; +import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; export interface SetupDependencies { data: DataPublicPluginSetup; - uiActions: UiActionsSetup; + advancedUiActions: AdvancedUiActionsSetup; } export interface StartDependencies { data: DataPublicPluginStart; - uiActions: UiActionsStart; + advancedUiActions: AdvancedUiActionsStart; } export class UiActionsEnhancedExamplesPlugin implements Plugin { - public setup(core: CoreSetup, plugins: SetupDependencies) { - // eslint-disable-next-line - console.log('ui_actions_enhanced_examples'); + public setup( + core: CoreSetup, + { advancedUiActions: uiActions }: SetupDependencies + ) { + const start = createStartServicesGetter(core.getStartServices); + + uiActions.registerDrilldown(new DashboardHelloWorldDrilldown()); + uiActions.registerDrilldown(new DashboardToUrlDrilldown()); + uiActions.registerDrilldown(new DashboardToDiscoverDrilldown({ start })); } public start(core: CoreStart, plugins: StartDependencies) {} diff --git a/x-pack/legacy/plugins/canvas/public/application.tsx b/x-pack/legacy/plugins/canvas/public/application.tsx index f746a24e9b261..f71123cd28b90 100644 --- a/x-pack/legacy/plugins/canvas/public/application.tsx +++ b/x-pack/legacy/plugins/canvas/public/application.tsx @@ -130,7 +130,7 @@ export const initializeCanvas = async ( restoreAction = action; startPlugins.uiActions.detachAction(VALUE_CLICK_TRIGGER, action.id); - startPlugins.uiActions.attachAction(VALUE_CLICK_TRIGGER, emptyAction); + startPlugins.uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, emptyAction); } if (setupPlugins.usageCollection) { @@ -147,7 +147,7 @@ export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDe startPlugins.uiActions.detachAction(VALUE_CLICK_TRIGGER, emptyAction.id); if (restoreAction) { - startPlugins.uiActions.attachAction(VALUE_CLICK_TRIGGER, restoreAction); + startPlugins.uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, restoreAction); restoreAction = undefined; } diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss index 2ba6f9baca90d..87ec3f8fc7ec1 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.scss @@ -1,8 +1,3 @@ -.auaActionWizard__selectedActionFactoryContainer { - background-color: $euiColorLightestShade; - padding: $euiSize; -} - .auaActionWizard__actionFactoryItem { .euiKeyPadMenuItem__label { height: #{$euiSizeXL}; diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx index 62f16890cade2..9c73f07289dc9 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.story.tsx @@ -6,28 +6,26 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; -import { dashboardDrilldownActionFactory, Demo, urlDrilldownActionFactory } from './test_data'; +import { Demo, dashboardFactory, urlFactory } from './test_data'; storiesOf('components/ActionWizard', module) - .add('default', () => ( - - )) + .add('default', () => ) .add('Only one factory is available', () => ( // to make sure layout doesn't break - + )) .add('Long list of action factories', () => ( // to make sure layout doesn't break )); diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx index aea47be693b8f..f43d832b1edae 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.test.tsx @@ -8,24 +8,17 @@ import React from 'react'; import { cleanup, fireEvent, render } from '@testing-library/react/pure'; import '@testing-library/jest-dom/extend-expect'; // TODO: this should be global import { TEST_SUBJ_ACTION_FACTORY_ITEM, TEST_SUBJ_SELECTED_ACTION_FACTORY } from './action_wizard'; -import { - dashboardDrilldownActionFactory, - dashboards, - Demo, - urlDrilldownActionFactory, -} from './test_data'; +import { dashboardFactory, dashboards, Demo, urlFactory } from './test_data'; // TODO: afterEach is not available for it globally during setup // https://github.com/elastic/kibana/issues/59469 afterEach(cleanup); test('Pick and configure action', () => { - const screen = render( - - ); + const screen = render(); // check that all factories are displayed to pick - expect(screen.getAllByTestId(TEST_SUBJ_ACTION_FACTORY_ITEM)).toHaveLength(2); + expect(screen.getAllByTestId(new RegExp(TEST_SUBJ_ACTION_FACTORY_ITEM))).toHaveLength(2); // select URL one fireEvent.click(screen.getByText(/Go to URL/i)); @@ -47,11 +40,11 @@ test('Pick and configure action', () => { }); test('If only one actions factory is available then actionFactory selection is emitted without user input', () => { - const screen = render(); + const screen = render(); // check that no factories are displayed to pick from - expect(screen.queryByTestId(TEST_SUBJ_ACTION_FACTORY_ITEM)).not.toBeInTheDocument(); - expect(screen.queryByTestId(TEST_SUBJ_SELECTED_ACTION_FACTORY)).toBeInTheDocument(); + expect(screen.queryByTestId(new RegExp(TEST_SUBJ_ACTION_FACTORY_ITEM))).not.toBeInTheDocument(); + expect(screen.queryByTestId(new RegExp(TEST_SUBJ_SELECTED_ACTION_FACTORY))).toBeInTheDocument(); // Input url const URL = 'https://elastic.co'; diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx index ef4a0f76de9ed..867ead688d23d 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx @@ -16,40 +16,20 @@ import { } from '@elastic/eui'; import { txtChangeButton } from './i18n'; import './action_wizard.scss'; - -// TODO: this interface is temporary for just moving forward with the component -// and it will be imported from the ../ui_actions when implemented properly -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type ActionBaseConfig = {}; -export interface ActionFactory { - type: string; // TODO: type should be tied to Action and ActionByType - displayName: string; - iconType?: string; - wizard: React.FC>; - createConfig: () => Config; - isValid: (config: Config) => boolean; -} - -export interface ActionFactoryWizardProps { - config?: Config; - - /** - * Callback called when user updates the config in UI. - */ - onConfig: (config: Config) => void; -} +import { ActionFactory } from '../../dynamic_actions'; export interface ActionWizardProps { /** * List of available action factories */ - actionFactories: Array>; // any here to be able to pass array of ActionFactory with different configs + actionFactories: ActionFactory[]; /** * Currently selected action factory - * undefined - is allowed and means that non is selected + * undefined - is allowed and means that none is selected */ currentActionFactory?: ActionFactory; + /** * Action factory selected changed * null - means user click "change" and removed action factory selection @@ -59,12 +39,17 @@ export interface ActionWizardProps { /** * current config for currently selected action factory */ - config?: ActionBaseConfig; + config?: object; /** * config changed */ - onConfigChange: (config: ActionBaseConfig) => void; + onConfigChange: (config: object) => void; + + /** + * Context will be passed into ActionFactory's methods + */ + context: object; } export const ActionWizard: React.FC = ({ @@ -73,6 +58,7 @@ export const ActionWizard: React.FC = ({ onActionFactoryChange, onConfigChange, config, + context, }) => { // auto pick action factory if there is only 1 available if (!currentActionFactory && actionFactories.length === 1) { @@ -87,6 +73,7 @@ export const ActionWizard: React.FC = ({ onDeselect={() => { onActionFactoryChange(null); }} + context={context} config={config} onConfigChange={newConfig => { onConfigChange(newConfig); @@ -97,6 +84,7 @@ export const ActionWizard: React.FC = ({ return ( { onActionFactoryChange(actionFactory); @@ -105,15 +93,16 @@ export const ActionWizard: React.FC = ({ ); }; -interface SelectedActionFactoryProps { - actionFactory: ActionFactory; - config: Config; - onConfigChange: (config: Config) => void; +interface SelectedActionFactoryProps { + actionFactory: ActionFactory; + config: object; + context: object; + onConfigChange: (config: object) => void; showDeselect: boolean; onDeselect: () => void; } -export const TEST_SUBJ_SELECTED_ACTION_FACTORY = 'selected-action-factory'; +export const TEST_SUBJ_SELECTED_ACTION_FACTORY = 'selectedActionFactory'; const SelectedActionFactory: React.FC = ({ actionFactory, @@ -121,28 +110,28 @@ const SelectedActionFactory: React.FC = ({ showDeselect, onConfigChange, config, + context, }) => { return (
    - {actionFactory.iconType && ( + {actionFactory.getIconType(context) && ( - + )} -

    {actionFactory.displayName}

    +

    {actionFactory.getDisplayName(context)}

    {showDeselect && ( - onDeselect()}> + onDeselect()}> {txtChangeButton} @@ -151,10 +140,11 @@ const SelectedActionFactory: React.FC = ({
    - {actionFactory.wizard({ - config, - onConfig: onConfigChange, - })} +
    ); @@ -162,14 +152,16 @@ const SelectedActionFactory: React.FC = ({ interface ActionFactorySelectorProps { actionFactories: ActionFactory[]; + context: object; onActionFactorySelected: (actionFactory: ActionFactory) => void; } -export const TEST_SUBJ_ACTION_FACTORY_ITEM = 'action-factory-item'; +export const TEST_SUBJ_ACTION_FACTORY_ITEM = 'actionFactoryItem'; const ActionFactorySelector: React.FC = ({ actionFactories, onActionFactorySelected, + context, }) => { if (actionFactories.length === 0) { // this is not user facing, as it would be impossible to get into this state @@ -177,20 +169,30 @@ const ActionFactorySelector: React.FC = ({ return
    No action factories to pick from
    ; } + // The below style is applied to fix Firefox rendering bug. + // See: https://github.com/elastic/kibana/pull/61219/#pullrequestreview-402903330 + const firefoxBugFix = { + willChange: 'opacity', + }; + return ( - - {actionFactories.map(actionFactory => ( - onActionFactorySelected(actionFactory)} - > - {actionFactory.iconType && } - - ))} + + {[...actionFactories] + .sort((f1, f2) => f2.order - f1.order) + .map(actionFactory => ( + + onActionFactorySelected(actionFactory)} + > + {actionFactory.getIconType(context) && ( + + )} + + + ))} ); }; diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts index 641f25176264a..a315184bf68ef 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/i18n.ts @@ -9,6 +9,6 @@ import { i18n } from '@kbn/i18n'; export const txtChangeButton = i18n.translate( 'xpack.advancedUiActions.components.actionWizard.changeButton', { - defaultMessage: 'change', + defaultMessage: 'Change', } ); diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts index ed224248ec4cd..a189afbf956ee 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ActionFactory, ActionWizard } from './action_wizard'; +export { ActionWizard } from './action_wizard'; diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx index 8ecdde681069e..c3e749f163c94 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/test_data.tsx @@ -6,124 +6,161 @@ import React, { useState } from 'react'; import { EuiFieldText, EuiFormRow, EuiSelect, EuiSwitch } from '@elastic/eui'; -import { ActionFactory, ActionBaseConfig, ActionWizard } from './action_wizard'; +import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; +import { ActionWizard } from './action_wizard'; +import { ActionFactoryDefinition, ActionFactory } from '../../dynamic_actions'; +import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public'; + +type ActionBaseConfig = object; export const dashboards = [ { id: 'dashboard1', title: 'Dashboard 1' }, { id: 'dashboard2', title: 'Dashboard 2' }, ]; -export const dashboardDrilldownActionFactory: ActionFactory<{ +interface DashboardDrilldownConfig { dashboardId?: string; - useCurrentDashboardFilters: boolean; - useCurrentDashboardDataRange: boolean; -}> = { - type: 'Dashboard', - displayName: 'Go to Dashboard', - iconType: 'dashboardApp', + useCurrentFilters: boolean; + useCurrentDateRange: boolean; +} + +function DashboardDrilldownCollectConfig(props: CollectConfigProps) { + const config = props.config ?? { + dashboardId: undefined, + useCurrentFilters: true, + useCurrentDateRange: true, + }; + return ( + <> + + ({ value: id, text: title }))} + value={config.dashboardId} + onChange={e => { + props.onConfig({ ...config, dashboardId: e.target.value }); + }} + /> + + + + props.onConfig({ + ...config, + useCurrentFilters: !config.useCurrentFilters, + }) + } + /> + + + + props.onConfig({ + ...config, + useCurrentDateRange: !config.useCurrentDateRange, + }) + } + /> + + + ); +} + +export const dashboardDrilldownActionFactory: ActionFactoryDefinition< + DashboardDrilldownConfig, + any, + any +> = { + id: 'Dashboard', + getDisplayName: () => 'Go to Dashboard', + getIconType: () => 'dashboardApp', createConfig: () => { return { dashboardId: undefined, - useCurrentDashboardDataRange: true, - useCurrentDashboardFilters: true, + useCurrentFilters: true, + useCurrentDateRange: true, }; }, - isValid: config => { + isConfigValid: (config: DashboardDrilldownConfig): config is DashboardDrilldownConfig => { if (!config.dashboardId) return false; return true; }, - wizard: props => { - const config = props.config ?? { - dashboardId: undefined, - useCurrentDashboardDataRange: true, - useCurrentDashboardFilters: true, - }; - return ( - <> - - ({ value: id, text: title }))} - value={config.dashboardId} - onChange={e => { - props.onConfig({ ...config, dashboardId: e.target.value }); - }} - /> - - - - props.onConfig({ - ...config, - useCurrentDashboardFilters: !config.useCurrentDashboardFilters, - }) - } - /> - - - - props.onConfig({ - ...config, - useCurrentDashboardDataRange: !config.useCurrentDashboardDataRange, - }) - } - /> - - - ); + CollectConfig: reactToUiComponent(DashboardDrilldownCollectConfig), + + isCompatible(context?: object): Promise { + return Promise.resolve(true); }, + order: 0, + create: () => ({ + id: 'test', + execute: async () => alert('Navigate to dashboard!'), + }), }; -export const urlDrilldownActionFactory: ActionFactory<{ url: string; openInNewTab: boolean }> = { - type: 'Url', - displayName: 'Go to URL', - iconType: 'link', +export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory); + +interface UrlDrilldownConfig { + url: string; + openInNewTab: boolean; +} +function UrlDrilldownCollectConfig(props: CollectConfigProps) { + const config = props.config ?? { + url: '', + openInNewTab: false, + }; + return ( + <> + + props.onConfig({ ...config, url: event.target.value })} + /> + + + props.onConfig({ ...config, openInNewTab: !config.openInNewTab })} + /> + + + ); +} +export const urlDrilldownActionFactory: ActionFactoryDefinition = { + id: 'Url', + getDisplayName: () => 'Go to URL', + getIconType: () => 'link', createConfig: () => { return { url: '', openInNewTab: false, }; }, - isValid: config => { + isConfigValid: (config: UrlDrilldownConfig): config is UrlDrilldownConfig => { if (!config.url) return false; return true; }, - wizard: props => { - const config = props.config ?? { - url: '', - openInNewTab: false, - }; - return ( - <> - - props.onConfig({ ...config, url: event.target.value })} - /> - - - props.onConfig({ ...config, openInNewTab: !config.openInNewTab })} - /> - - - ); + CollectConfig: reactToUiComponent(UrlDrilldownCollectConfig), + + order: 10, + isCompatible(context?: object): Promise { + return Promise.resolve(true); }, + create: () => null as any, }; +export const urlFactory = new ActionFactory(urlDrilldownActionFactory); + export function Demo({ actionFactories }: { actionFactories: Array> }) { const [state, setState] = useState<{ currentActionFactory?: ActionFactory; @@ -157,14 +194,15 @@ export function Demo({ actionFactories }: { actionFactories: Array

    -
    Action Factory Type: {state.currentActionFactory?.type}
    +
    Action Factory Id: {state.currentActionFactory?.id}
    Action Factory Config: {JSON.stringify(state.config)}
    Is config valid:{' '} - {JSON.stringify(state.currentActionFactory?.isValid(state.config!) ?? false)} + {JSON.stringify(state.currentActionFactory?.isConfigValid(state.config!) ?? false)}
    ); diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx b/x-pack/plugins/advanced_ui_actions/public/components/index.ts similarity index 87% rename from x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx rename to x-pack/plugins/advanced_ui_actions/public/components/index.ts index 3be289fe6d46e..236b1a6ec4611 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_picker/index.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './drilldown_picker'; +export * from './action_wizard'; diff --git a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx index 325a5ddc10179..c0cd8d5540db2 100644 --- a/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/custom_time_range_action.tsx @@ -44,7 +44,7 @@ export class CustomTimeRangeAction implements ActionByType { + /** + * Globally unique identifier for this drilldown. + */ + id: string; + + /** + * Determines the display order of the drilldowns in the flyout picker. + * Higher numbers are displayed first. + */ + order?: number; + + /** + * Function that returns default config for this drilldown. + */ + createConfig: ActionFactoryDefinition['createConfig']; + + /** + * `UiComponent` that collections config for this drilldown. You can create + * a React component and transform it `UiComponent` using `uiToReactComponent` + * helper from `kibana_utils` plugin. + * + * ```tsx + * import React from 'react'; + * import { uiToReactComponent } from 'src/plugins/kibana_utils'; + * import { CollectConfigProps } from 'src/plugins/kibana_utils/public'; + * + * type Props = CollectConfigProps; + * + * const ReactCollectConfig: React.FC = () => { + * return
    Collecting config...'
    ; + * }; + * + * export const CollectConfig = uiToReactComponent(ReactCollectConfig); + * ``` + */ + CollectConfig: ActionFactoryDefinition['CollectConfig']; + + /** + * A validator function for the config object. Should always return a boolean + * given any input. + */ + isConfigValid: ActionFactoryDefinition['isConfigValid']; + + /** + * Name of EUI icon to display when showing this drilldown to user. + */ + euiIcon?: string; + + /** + * Should return an internationalized name of the drilldown, which will be + * displayed to the user. + */ + getDisplayName: () => string; + + /** + * Implements the "navigation" action of the drilldown. This happens when + * user clicks something in the UI that executes a trigger to which this + * drilldown was attached. + * + * @param config Config object that user configured this drilldown with. + * @param context Object that represents context in which the underlying + * `UIAction` of this drilldown is being executed in. + */ + execute(config: Config, context: ExecutionContext): void; + + /** + * A link where drilldown should navigate on middle click or Ctrl + click. + */ + getHref?(config: Config, context: ExecutionContext): Promise; +} diff --git a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/index.ts b/x-pack/plugins/advanced_ui_actions/public/drilldowns/index.ts similarity index 84% rename from x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/index.ts rename to x-pack/plugins/advanced_ui_actions/public/drilldowns/index.ts index ce235043b4ef6..7f81a68c803eb 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/index.ts +++ b/x-pack/plugins/advanced_ui_actions/public/drilldowns/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './flyout_create_drilldown'; +export * from './drilldown_definition'; diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory.ts b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory.ts new file mode 100644 index 0000000000000..f1aef5deff49e --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory.ts @@ -0,0 +1,55 @@ +/* + * 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 { uiToReactComponent } from '../../../../../src/plugins/kibana_react/public'; +import { + UiActionsActionDefinition as ActionDefinition, + UiActionsPresentable as Presentable, +} from '../../../../../src/plugins/ui_actions/public'; +import { ActionFactoryDefinition } from './action_factory_definition'; +import { Configurable } from '../../../../../src/plugins/kibana_utils/public'; +import { SerializedAction } from './types'; + +export class ActionFactory< + Config extends object = object, + FactoryContext extends object = object, + ActionContext extends object = object +> implements Omit, 'getHref'>, Configurable { + constructor( + protected readonly def: ActionFactoryDefinition + ) {} + + public readonly id = this.def.id; + public readonly order = this.def.order || 0; + public readonly MenuItem? = this.def.MenuItem; + public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined; + + public readonly CollectConfig = this.def.CollectConfig; + public readonly ReactCollectConfig = uiToReactComponent(this.CollectConfig); + public readonly createConfig = this.def.createConfig; + public readonly isConfigValid = this.def.isConfigValid; + + public getIconType(context: FactoryContext): string | undefined { + if (!this.def.getIconType) return undefined; + return this.def.getIconType(context); + } + + public getDisplayName(context: FactoryContext): string { + if (!this.def.getDisplayName) return ''; + return this.def.getDisplayName(context); + } + + public async isCompatible(context: FactoryContext): Promise { + if (!this.def.isCompatible) return true; + return await this.def.isCompatible(context); + } + + public create( + serializedAction: Omit, 'factoryId'> + ): ActionDefinition { + return this.def.create(serializedAction); + } +} diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory_definition.ts b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory_definition.ts new file mode 100644 index 0000000000000..d3751fe811665 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/action_factory_definition.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + UiActionsActionDefinition as ActionDefinition, + UiActionsPresentable as Presentable, +} from '../../../../../src/plugins/ui_actions/public'; +import { Configurable } from '../../../../../src/plugins/kibana_utils/public'; +import { SerializedAction } from './types'; + +/** + * This is a convenience interface for registering new action factories. + */ +export interface ActionFactoryDefinition< + Config extends object = object, + FactoryContext extends object = object, + ActionContext extends object = object +> + extends Partial, 'getHref'>>, + Configurable { + /** + * Unique ID of the action factory. This ID is used to identify this action + * factory in the registry as well as to construct actions of this type and + * identify this action factory when presenting it to the user in UI. + */ + id: string; + + /** + * This method should return a definition of a new action, normally used to + * register it in `ui_actions` registry. + */ + create( + serializedAction: Omit, 'factoryId'> + ): ActionDefinition; +} diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager.test.ts new file mode 100644 index 0000000000000..b7f1b36f8f358 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager.test.ts @@ -0,0 +1,635 @@ +/* + * 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 { DynamicActionManager } from './dynamic_action_manager'; +import { ActionStorage, MemoryActionStorage } from './dynamic_action_storage'; +import { UiActionsService } from '../../../../../src/plugins/ui_actions/public'; +import { ActionInternal } from '../../../../../src/plugins/ui_actions/public/actions'; +import { of } from '../../../../../src/plugins/kibana_utils'; +import { UiActionsServiceEnhancements } from '../services'; +import { ActionFactoryDefinition } from './action_factory_definition'; +import { SerializedAction, SerializedEvent } from './types'; + +const actionFactoryDefinition1: ActionFactoryDefinition = { + id: 'ACTION_FACTORY_1', + CollectConfig: {} as any, + createConfig: () => ({}), + isConfigValid: (() => true) as any, + create: ({ name }) => ({ + id: '', + execute: async () => {}, + getDisplayName: () => name, + }), +}; + +const actionFactoryDefinition2: ActionFactoryDefinition = { + id: 'ACTION_FACTORY_2', + CollectConfig: {} as any, + createConfig: () => ({}), + isConfigValid: (() => true) as any, + create: ({ name }) => ({ + id: '', + execute: async () => {}, + getDisplayName: () => name, + }), +}; + +const event1: SerializedEvent = { + eventId: 'EVENT_ID_1', + triggers: ['VALUE_CLICK_TRIGGER'], + action: { + factoryId: actionFactoryDefinition1.id, + name: 'Action 1', + config: {}, + }, +}; + +const event2: SerializedEvent = { + eventId: 'EVENT_ID_2', + triggers: ['VALUE_CLICK_TRIGGER'], + action: { + factoryId: actionFactoryDefinition1.id, + name: 'Action 2', + config: {}, + }, +}; + +const event3: SerializedEvent = { + eventId: 'EVENT_ID_3', + triggers: ['VALUE_CLICK_TRIGGER'], + action: { + factoryId: actionFactoryDefinition2.id, + name: 'Action 3', + config: {}, + }, +}; + +const setup = (events: readonly SerializedEvent[] = []) => { + const isCompatible = async () => true; + const storage: ActionStorage = new MemoryActionStorage(events); + const actions = new Map(); + const uiActions = new UiActionsService({ + actions, + }); + const uiActionsEnhancements = new UiActionsServiceEnhancements(); + const manager = new DynamicActionManager({ + isCompatible, + storage, + uiActions: { ...uiActions, ...uiActionsEnhancements }, + }); + + uiActions.registerTrigger({ + id: 'VALUE_CLICK_TRIGGER', + }); + + return { + isCompatible, + actions, + storage, + uiActions: { ...uiActions, ...uiActionsEnhancements }, + manager, + }; +}; + +describe('DynamicActionManager', () => { + test('can instantiate', () => { + const { manager } = setup([event1]); + expect(manager).toBeInstanceOf(DynamicActionManager); + }); + + describe('.start()', () => { + test('instantiates stored events', async () => { + const { manager, actions, uiActions } = setup([event1]); + const create1 = jest.fn(); + const create2 = jest.fn(); + + uiActions.registerActionFactory({ ...actionFactoryDefinition1, create: create1 }); + uiActions.registerActionFactory({ ...actionFactoryDefinition2, create: create2 }); + + expect(create1).toHaveBeenCalledTimes(0); + expect(create2).toHaveBeenCalledTimes(0); + expect(actions.size).toBe(0); + + await manager.start(); + + expect(create1).toHaveBeenCalledTimes(1); + expect(create2).toHaveBeenCalledTimes(0); + expect(actions.size).toBe(1); + }); + + test('does nothing when no events stored', async () => { + const { manager, actions, uiActions } = setup(); + const create1 = jest.fn(); + const create2 = jest.fn(); + + uiActions.registerActionFactory({ ...actionFactoryDefinition1, create: create1 }); + uiActions.registerActionFactory({ ...actionFactoryDefinition2, create: create2 }); + + expect(create1).toHaveBeenCalledTimes(0); + expect(create2).toHaveBeenCalledTimes(0); + expect(actions.size).toBe(0); + + await manager.start(); + + expect(create1).toHaveBeenCalledTimes(0); + expect(create2).toHaveBeenCalledTimes(0); + expect(actions.size).toBe(0); + }); + + test('UI state is empty before manager starts', async () => { + const { manager } = setup([event1]); + + expect(manager.state.get()).toMatchObject({ + events: [], + isFetchingEvents: false, + fetchCount: 0, + }); + }); + + test('loads events into UI state', async () => { + const { manager, uiActions } = setup([event1, event2, event3]); + + uiActions.registerActionFactory(actionFactoryDefinition1); + uiActions.registerActionFactory(actionFactoryDefinition2); + + await manager.start(); + + expect(manager.state.get()).toMatchObject({ + events: [event1, event2, event3], + isFetchingEvents: false, + fetchCount: 1, + }); + }); + + test('sets isFetchingEvents to true while fetching events', async () => { + const { manager, uiActions } = setup([event1, event2, event3]); + + uiActions.registerActionFactory(actionFactoryDefinition1); + uiActions.registerActionFactory(actionFactoryDefinition2); + + const promise = manager.start().catch(() => {}); + + expect(manager.state.get().isFetchingEvents).toBe(true); + + await promise; + + expect(manager.state.get().isFetchingEvents).toBe(false); + }); + + test('throws if storage threw', async () => { + const { manager, storage } = setup([event1]); + + storage.list = async () => { + throw new Error('baz'); + }; + + const [, error] = await of(manager.start()); + + expect(error).toEqual(new Error('baz')); + }); + + test('sets UI state error if error happened during initial fetch', async () => { + const { manager, storage } = setup([event1]); + + storage.list = async () => { + throw new Error('baz'); + }; + + await of(manager.start()); + + expect(manager.state.get().fetchError!.message).toBe('baz'); + }); + }); + + describe('.stop()', () => { + test('removes events from UI actions registry', async () => { + const { manager, actions, uiActions } = setup([event1, event2]); + const create1 = jest.fn(); + const create2 = jest.fn(); + + uiActions.registerActionFactory({ ...actionFactoryDefinition1, create: create1 }); + uiActions.registerActionFactory({ ...actionFactoryDefinition2, create: create2 }); + + expect(actions.size).toBe(0); + + await manager.start(); + + expect(actions.size).toBe(2); + + await manager.stop(); + + expect(actions.size).toBe(0); + }); + }); + + describe('.createEvent()', () => { + describe('when storage succeeds', () => { + test('stores new event in storage', async () => { + const { manager, storage, uiActions } = setup([]); + + uiActions.registerActionFactory(actionFactoryDefinition1); + await manager.start(); + + const action: SerializedAction = { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }; + + expect(await storage.count()).toBe(0); + + await manager.createEvent(action, ['VALUE_CLICK_TRIGGER']); + + expect(await storage.count()).toBe(1); + + const [event] = await storage.list(); + + expect(event).toMatchObject({ + eventId: expect.any(String), + triggers: ['VALUE_CLICK_TRIGGER'], + action: { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }, + }); + }); + + test('adds event to UI state', async () => { + const { manager, uiActions } = setup([]); + const action: SerializedAction = { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }; + + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(manager.state.get().events.length).toBe(0); + + await manager.createEvent(action, ['VALUE_CLICK_TRIGGER']); + + expect(manager.state.get().events.length).toBe(1); + }); + + test('optimistically adds event to UI state', async () => { + const { manager, uiActions } = setup([]); + const action: SerializedAction = { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }; + + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(manager.state.get().events.length).toBe(0); + + const promise = manager.createEvent(action, ['VALUE_CLICK_TRIGGER']).catch(e => e); + + expect(manager.state.get().events.length).toBe(1); + + await promise; + + expect(manager.state.get().events.length).toBe(1); + }); + + test('instantiates event in actions service', async () => { + const { manager, uiActions, actions } = setup([]); + const action: SerializedAction = { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }; + + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(actions.size).toBe(0); + + await manager.createEvent(action, ['VALUE_CLICK_TRIGGER']); + + expect(actions.size).toBe(1); + }); + }); + + describe('when storage fails', () => { + test('throws an error', async () => { + const { manager, storage, uiActions } = setup([]); + + storage.create = async () => { + throw new Error('foo'); + }; + + uiActions.registerActionFactory(actionFactoryDefinition1); + await manager.start(); + + const action: SerializedAction = { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }; + + const [, error] = await of(manager.createEvent(action, ['VALUE_CLICK_TRIGGER'])); + + expect(error).toEqual(new Error('foo')); + }); + + test('does not add even to UI state', async () => { + const { manager, storage, uiActions } = setup([]); + const action: SerializedAction = { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }; + + storage.create = async () => { + throw new Error('foo'); + }; + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + await of(manager.createEvent(action, ['VALUE_CLICK_TRIGGER'])); + + expect(manager.state.get().events.length).toBe(0); + }); + + test('optimistically adds event to UI state and then removes it', async () => { + const { manager, storage, uiActions } = setup([]); + const action: SerializedAction = { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }; + + storage.create = async () => { + throw new Error('foo'); + }; + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(manager.state.get().events.length).toBe(0); + + const promise = manager.createEvent(action, ['VALUE_CLICK_TRIGGER']).catch(e => e); + + expect(manager.state.get().events.length).toBe(1); + + await promise; + + expect(manager.state.get().events.length).toBe(0); + }); + + test('does not instantiate event in actions service', async () => { + const { manager, storage, uiActions, actions } = setup([]); + const action: SerializedAction = { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }; + + storage.create = async () => { + throw new Error('foo'); + }; + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(actions.size).toBe(0); + + await of(manager.createEvent(action, ['VALUE_CLICK_TRIGGER'])); + + expect(actions.size).toBe(0); + }); + }); + }); + + describe('.updateEvent()', () => { + describe('when storage succeeds', () => { + test('un-registers old event from ui actions service and registers the new one', async () => { + const { manager, actions, uiActions } = setup([event3]); + + uiActions.registerActionFactory(actionFactoryDefinition2); + await manager.start(); + + expect(actions.size).toBe(1); + + const registeredAction1 = actions.values().next().value; + + expect(registeredAction1.getDisplayName()).toBe('Action 3'); + + const action: SerializedAction = { + factoryId: actionFactoryDefinition2.id, + name: 'foo', + config: {}, + }; + + await manager.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER']); + + expect(actions.size).toBe(1); + + const registeredAction2 = actions.values().next().value; + + expect(registeredAction2.getDisplayName()).toBe('foo'); + }); + + test('updates event in storage', async () => { + const { manager, storage, uiActions } = setup([event3]); + const storageUpdateSpy = jest.spyOn(storage, 'update'); + + uiActions.registerActionFactory(actionFactoryDefinition2); + await manager.start(); + + const action: SerializedAction = { + factoryId: actionFactoryDefinition2.id, + name: 'foo', + config: {}, + }; + + expect(storageUpdateSpy).toHaveBeenCalledTimes(0); + + await manager.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER']); + + expect(storageUpdateSpy).toHaveBeenCalledTimes(1); + expect(storageUpdateSpy.mock.calls[0][0]).toMatchObject({ + eventId: expect.any(String), + triggers: ['VALUE_CLICK_TRIGGER'], + action: { + factoryId: actionFactoryDefinition2.id, + }, + }); + }); + + test('updates event in UI state', async () => { + const { manager, uiActions } = setup([event3]); + + uiActions.registerActionFactory(actionFactoryDefinition2); + await manager.start(); + + const action: SerializedAction = { + factoryId: actionFactoryDefinition2.id, + name: 'foo', + config: {}, + }; + + expect(manager.state.get().events[0].action.name).toBe('Action 3'); + + await manager.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER']); + + expect(manager.state.get().events[0].action.name).toBe('foo'); + }); + + test('optimistically updates event in UI state', async () => { + const { manager, uiActions } = setup([event3]); + + uiActions.registerActionFactory(actionFactoryDefinition2); + await manager.start(); + + const action: SerializedAction = { + factoryId: actionFactoryDefinition2.id, + name: 'foo', + config: {}, + }; + + expect(manager.state.get().events[0].action.name).toBe('Action 3'); + + const promise = manager + .updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER']) + .catch(e => e); + + expect(manager.state.get().events[0].action.name).toBe('foo'); + + await promise; + }); + }); + + describe('when storage fails', () => { + test('throws error', async () => { + const { manager, storage, uiActions } = setup([event3]); + + storage.update = () => { + throw new Error('bar'); + }; + uiActions.registerActionFactory(actionFactoryDefinition2); + await manager.start(); + + const action: SerializedAction = { + factoryId: actionFactoryDefinition2.id, + name: 'foo', + config: {}, + }; + + const [, error] = await of( + manager.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER']) + ); + + expect(error).toEqual(new Error('bar')); + }); + + test('keeps the old action in actions registry', async () => { + const { manager, storage, actions, uiActions } = setup([event3]); + + storage.update = () => { + throw new Error('bar'); + }; + uiActions.registerActionFactory(actionFactoryDefinition2); + await manager.start(); + + expect(actions.size).toBe(1); + + const registeredAction1 = actions.values().next().value; + + expect(registeredAction1.getDisplayName()).toBe('Action 3'); + + const action: SerializedAction = { + factoryId: actionFactoryDefinition2.id, + name: 'foo', + config: {}, + }; + + await of(manager.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER'])); + + expect(actions.size).toBe(1); + + const registeredAction2 = actions.values().next().value; + + expect(registeredAction2.getDisplayName()).toBe('Action 3'); + }); + + test('keeps old event in UI state', async () => { + const { manager, storage, uiActions } = setup([event3]); + + storage.update = () => { + throw new Error('bar'); + }; + uiActions.registerActionFactory(actionFactoryDefinition2); + await manager.start(); + + const action: SerializedAction = { + factoryId: actionFactoryDefinition2.id, + name: 'foo', + config: {}, + }; + + expect(manager.state.get().events[0].action.name).toBe('Action 3'); + + await of(manager.updateEvent(event3.eventId, action, ['VALUE_CLICK_TRIGGER'])); + + expect(manager.state.get().events[0].action.name).toBe('Action 3'); + }); + }); + }); + + describe('.deleteEvents()', () => { + describe('when storage succeeds', () => { + test('removes all actions from uiActions service', async () => { + const { manager, actions, uiActions } = setup([event2, event1]); + + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(actions.size).toBe(2); + + await manager.deleteEvents([event1.eventId, event2.eventId]); + + expect(actions.size).toBe(0); + }); + + test('removes all events from storage', async () => { + const { manager, uiActions, storage } = setup([event2, event1]); + + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(await storage.list()).toEqual([event2, event1]); + + await manager.deleteEvents([event1.eventId, event2.eventId]); + + expect(await storage.list()).toEqual([]); + }); + + test('removes all events from UI state', async () => { + const { manager, uiActions } = setup([event2, event1]); + + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(manager.state.get().events).toEqual([event2, event1]); + + await manager.deleteEvents([event1.eventId, event2.eventId]); + + expect(manager.state.get().events).toEqual([]); + }); + }); + }); +}); diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager.ts new file mode 100644 index 0000000000000..df214bfe80cc7 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager.ts @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { v4 as uuidv4 } from 'uuid'; +import { Subscription } from 'rxjs'; +import { ActionStorage } from './dynamic_action_storage'; +import { + TriggerContextMapping, + UiActionsActionDefinition as ActionDefinition, +} from '../../../../../src/plugins/ui_actions/public'; +import { defaultState, transitions, selectors, State } from './dynamic_action_manager_state'; +import { StateContainer, createStateContainer } from '../../../../../src/plugins/kibana_utils'; +import { StartContract } from '../plugin'; +import { SerializedAction, SerializedEvent } from './types'; + +const compareEvents = ( + a: ReadonlyArray<{ eventId: string }>, + b: ReadonlyArray<{ eventId: string }> +) => { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i].eventId !== b[i].eventId) return false; + return true; +}; + +export type DynamicActionManagerState = State; + +export interface DynamicActionManagerParams { + storage: ActionStorage; + uiActions: Pick< + StartContract, + 'registerAction' | 'attachAction' | 'unregisterAction' | 'detachAction' | 'getActionFactory' + >; + isCompatible: (context: C) => Promise; +} + +export class DynamicActionManager { + static idPrefixCounter = 0; + + private readonly idPrefix = `D_ACTION_${DynamicActionManager.idPrefixCounter++}_`; + private stopped: boolean = false; + private reloadSubscription?: Subscription; + + /** + * UI State of the dynamic action manager. + */ + protected readonly ui = createStateContainer(defaultState, transitions, selectors); + + constructor(protected readonly params: DynamicActionManagerParams) {} + + protected getEvent(eventId: string): SerializedEvent { + const oldEvent = this.ui.selectors.getEvent(eventId); + if (!oldEvent) throw new Error(`Could not find event [eventId = ${eventId}].`); + return oldEvent; + } + + /** + * We prefix action IDs with a unique `.idPrefix`, so we can render the + * same dashboard twice on the screen. + */ + protected generateActionId(eventId: string): string { + return this.idPrefix + eventId; + } + + protected reviveAction(event: SerializedEvent) { + const { eventId, triggers, action } = event; + const { uiActions, isCompatible } = this.params; + + const actionId = this.generateActionId(eventId); + const factory = uiActions.getActionFactory(event.action.factoryId); + const actionDefinition: ActionDefinition = { + ...factory.create(action as SerializedAction), + id: actionId, + isCompatible, + }; + + uiActions.registerAction(actionDefinition); + for (const trigger of triggers) uiActions.attachAction(trigger as any, actionId); + } + + protected killAction({ eventId, triggers }: SerializedEvent) { + const { uiActions } = this.params; + const actionId = this.generateActionId(eventId); + + for (const trigger of triggers) uiActions.detachAction(trigger as any, actionId); + uiActions.unregisterAction(actionId); + } + + private syncId = 0; + + /** + * This function is called every time stored events might have changed not by + * us. For example, when in edit mode on dashboard user presses "back" button + * in the browser, then contents of storage changes. + */ + private onSync = () => { + if (this.stopped) return; + + (async () => { + const syncId = ++this.syncId; + const events = await this.params.storage.list(); + + if (this.stopped) return; + if (syncId !== this.syncId) return; + if (compareEvents(events, this.ui.get().events)) return; + + for (const event of this.ui.get().events) this.killAction(event); + for (const event of events) this.reviveAction(event); + this.ui.transitions.finishFetching(events); + })().catch(error => { + /* eslint-disable */ + console.log('Dynamic action manager storage reload failed.'); + console.error(error); + /* eslint-enable */ + }); + }; + + // Public API: --------------------------------------------------------------- + + /** + * Read-only state container of dynamic action manager. Use it to perform all + * *read* operations. + */ + public readonly state: StateContainer = this.ui; + + /** + * 1. Loads all events from @type {DynamicActionStorage} storage. + * 2. Creates actions for each event in `ui_actions` registry. + * 3. Adds events to UI state. + * 4. Does nothing if dynamic action manager was stopped or if event fetching + * is already taking place. + */ + public async start() { + if (this.stopped) return; + if (this.ui.get().isFetchingEvents) return; + + this.ui.transitions.startFetching(); + try { + const events = await this.params.storage.list(); + for (const event of events) this.reviveAction(event); + this.ui.transitions.finishFetching(events); + } catch (error) { + this.ui.transitions.failFetching(error instanceof Error ? error : { message: String(error) }); + throw error; + } + + if (this.params.storage.reload$) { + this.reloadSubscription = this.params.storage.reload$.subscribe(this.onSync); + } + } + + /** + * 1. Removes all events from `ui_actions` registry. + * 2. Puts dynamic action manager is stopped state. + */ + public async stop() { + this.stopped = true; + const events = await this.params.storage.list(); + + for (const event of events) { + this.killAction(event); + } + + if (this.reloadSubscription) { + this.reloadSubscription.unsubscribe(); + } + } + + /** + * Creates a new event. + * + * 1. Stores event in @type {DynamicActionStorage} storage. + * 2. Optimistically adds it to UI state, and rolls back on failure. + * 3. Adds action to `ui_actions` registry. + * + * @param action Dynamic action for which to create an event. + * @param triggers List of triggers to which action should react. + */ + public async createEvent( + action: SerializedAction, + triggers: Array + ) { + const event: SerializedEvent = { + eventId: uuidv4(), + triggers, + action, + }; + + this.ui.transitions.addEvent(event); + try { + await this.params.storage.create(event); + this.reviveAction(event); + } catch (error) { + this.ui.transitions.removeEvent(event.eventId); + throw error; + } + } + + /** + * Updates an existing event. Fails if event with given `eventId` does not + * exit. + * + * 1. Updates the event in @type {DynamicActionStorage} storage. + * 2. Optimistically replaces the old event by the new one in UI state, and + * rolls back on failure. + * 3. Replaces action in `ui_actions` registry with the new event. + * + * + * @param eventId ID of the event to replace. + * @param action New action for which to create the event. + * @param triggers List of triggers to which action should react. + */ + public async updateEvent( + eventId: string, + action: SerializedAction, + triggers: Array + ) { + const event: SerializedEvent = { + eventId, + triggers, + action, + }; + const oldEvent = this.getEvent(eventId); + this.killAction(oldEvent); + + this.reviveAction(event); + this.ui.transitions.replaceEvent(event); + + try { + await this.params.storage.update(event); + } catch (error) { + this.killAction(event); + this.reviveAction(oldEvent); + this.ui.transitions.replaceEvent(oldEvent); + throw error; + } + } + + /** + * Removes existing event. Throws if event does not exist. + * + * 1. Removes the event from @type {DynamicActionStorage} storage. + * 2. Optimistically removes event from UI state, and puts it back on failure. + * 3. Removes associated action from `ui_actions` registry. + * + * @param eventId ID of the event to remove. + */ + public async deleteEvent(eventId: string) { + const event = this.getEvent(eventId); + + this.killAction(event); + this.ui.transitions.removeEvent(eventId); + + try { + await this.params.storage.remove(eventId); + } catch (error) { + this.reviveAction(event); + this.ui.transitions.addEvent(event); + throw error; + } + } + + /** + * Deletes multiple events at once. + * + * @param eventIds List of event IDs. + */ + public async deleteEvents(eventIds: string[]) { + await Promise.all(eventIds.map(this.deleteEvent.bind(this))); + } +} diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager_state.ts b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager_state.ts new file mode 100644 index 0000000000000..61e8604baa913 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_manager_state.ts @@ -0,0 +1,98 @@ +/* + * 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 { SerializedEvent } from './types'; + +/** + * This interface represents the state of @type {DynamicActionManager} at any + * point in time. + */ +export interface State { + /** + * Whether dynamic action manager is currently in process of fetching events + * from storage. + */ + readonly isFetchingEvents: boolean; + + /** + * Number of times event fetching has been completed. + */ + readonly fetchCount: number; + + /** + * Error received last time when fetching events. + */ + readonly fetchError?: { + message: string; + }; + + /** + * List of all fetched events. + */ + readonly events: readonly SerializedEvent[]; +} + +export interface Transitions { + startFetching: (state: State) => () => State; + finishFetching: (state: State) => (events: SerializedEvent[]) => State; + failFetching: (state: State) => (error: { message: string }) => State; + addEvent: (state: State) => (event: SerializedEvent) => State; + removeEvent: (state: State) => (eventId: string) => State; + replaceEvent: (state: State) => (event: SerializedEvent) => State; +} + +export interface Selectors { + getEvent: (state: State) => (eventId: string) => SerializedEvent | null; +} + +export const defaultState: State = { + isFetchingEvents: false, + fetchCount: 0, + events: [], +}; + +export const transitions: Transitions = { + startFetching: state => () => ({ ...state, isFetchingEvents: true }), + + finishFetching: state => events => ({ + ...state, + isFetchingEvents: false, + fetchCount: state.fetchCount + 1, + fetchError: undefined, + events, + }), + + failFetching: state => ({ message }) => ({ + ...state, + isFetchingEvents: false, + fetchCount: state.fetchCount + 1, + fetchError: { message }, + }), + + addEvent: state => (event: SerializedEvent) => ({ + ...state, + events: [...state.events, event], + }), + + removeEvent: state => (eventId: string) => ({ + ...state, + events: state.events ? state.events.filter(event => event.eventId !== eventId) : state.events, + }), + + replaceEvent: state => event => { + const index = state.events.findIndex(({ eventId }) => eventId === event.eventId); + if (index === -1) return state; + + return { + ...state, + events: [...state.events.slice(0, index), event, ...state.events.slice(index + 1)], + }; + }, +}; + +export const selectors: Selectors = { + getEvent: state => eventId => state.events.find(event => event.eventId === eventId) || null, +}; diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_storage.ts b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_storage.ts new file mode 100644 index 0000000000000..e40441e67f033 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/dynamic_action_storage.ts @@ -0,0 +1,80 @@ +/* + * 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 max-classes-per-file */ + +import { Observable, Subject } from 'rxjs'; +import { SerializedEvent } from './types'; + +/** + * This CRUD interface needs to be implemented by dynamic action users if they + * want to persist the dynamic actions. It has a default implementation in + * Embeddables, however one can use the dynamic actions without Embeddables, + * in that case they have to implement this interface. + */ +export interface ActionStorage { + create(event: SerializedEvent): Promise; + update(event: SerializedEvent): Promise; + remove(eventId: string): Promise; + read(eventId: string): Promise; + count(): Promise; + list(): Promise; + + /** + * Triggered every time events changed in storage and should be re-loaded. + */ + readonly reload$?: Observable; +} + +export abstract class AbstractActionStorage implements ActionStorage { + public readonly reload$: Observable & Pick, 'next'> = new Subject(); + + public async count(): Promise { + return (await this.list()).length; + } + + public async read(eventId: string): Promise { + const events = await this.list(); + const event = events.find(ev => ev.eventId === eventId); + if (!event) throw new Error(`Event [eventId = ${eventId}] not found.`); + return event; + } + + abstract create(event: SerializedEvent): Promise; + abstract update(event: SerializedEvent): Promise; + abstract remove(eventId: string): Promise; + abstract list(): Promise; +} + +/** + * This is an in-memory implementation of ActionStorage. It is used in testing, + * but can also be used production code to store events in memory. + */ +export class MemoryActionStorage extends AbstractActionStorage { + constructor(public events: readonly SerializedEvent[] = []) { + super(); + } + + public async list() { + return this.events.map(event => ({ ...event })); + } + + public async create(event: SerializedEvent) { + this.events = [...this.events, { ...event }]; + } + + public async update(event: SerializedEvent) { + const index = this.events.findIndex(({ eventId }) => eventId === event.eventId); + if (index < 0) throw new Error(`Event [eventId = ${event.eventId}] not found`); + this.events = [...this.events.slice(0, index), { ...event }, ...this.events.slice(index + 1)]; + } + + public async remove(eventId: string) { + const index = this.events.findIndex(ev => eventId === ev.eventId); + if (index < 0) throw new Error(`Event [eventId = ${eventId}] not found`); + this.events = [...this.events.slice(0, index), ...this.events.slice(index + 1)]; + } +} diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/index.ts b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/index.ts new file mode 100644 index 0000000000000..bb37cf5e69535 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './types'; +export * from './action_factory'; +export * from './action_factory_definition'; +export * from './dynamic_action_storage'; +export * from './dynamic_action_manager_state'; +export * from './dynamic_action_manager'; diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/types.ts b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/types.ts new file mode 100644 index 0000000000000..9148d1ec7055a --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/types.ts @@ -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. + */ + +export interface SerializedAction { + readonly factoryId: string; + readonly name: string; + readonly config: Config; +} + +/** + * Serialized representation of a triggers-action pair, used to persist in storage. + */ +export interface SerializedEvent { + eventId: string; + triggers: string[]; + action: SerializedAction; +} diff --git a/x-pack/plugins/advanced_ui_actions/public/index.ts b/x-pack/plugins/advanced_ui_actions/public/index.ts index c11c1119a9b13..024cfe5530b97 100644 --- a/x-pack/plugins/advanced_ui_actions/public/index.ts +++ b/x-pack/plugins/advanced_ui_actions/public/index.ts @@ -12,3 +12,22 @@ export function plugin(initializerContext: PluginInitializerContext) { } export { AdvancedUiActionsPublicPlugin as Plugin }; +export { + SetupContract as AdvancedUiActionsSetup, + StartContract as AdvancedUiActionsStart, +} from './plugin'; + +export { ActionWizard } from './components'; +export { + ActionFactoryDefinition as AdvancedUiActionsActionFactoryDefinition, + ActionFactory as AdvancedUiActionsActionFactory, + SerializedAction as UiActionsEnhancedSerializedAction, + SerializedEvent as UiActionsEnhancedSerializedEvent, + AbstractActionStorage as UiActionsEnhancedAbstractActionStorage, + DynamicActionManager as UiActionsEnhancedDynamicActionManager, + DynamicActionManagerParams as UiActionsEnhancedDynamicActionManagerParams, + DynamicActionManagerState as UiActionsEnhancedDynamicActionManagerState, + MemoryActionStorage as UiActionsEnhancedMemoryActionStorage, +} from './dynamic_actions'; + +export { DrilldownDefinition as UiActionsEnhancedDrilldownDefinition } from './drilldowns'; diff --git a/x-pack/plugins/advanced_ui_actions/public/mocks.ts b/x-pack/plugins/advanced_ui_actions/public/mocks.ts new file mode 100644 index 0000000000000..65fde12755beb --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/mocks.ts @@ -0,0 +1,73 @@ +/* + * 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 { CoreSetup, CoreStart } from '../../../../src/core/public'; +import { coreMock } from '../../../../src/core/public/mocks'; +import { uiActionsPluginMock } from '../../../../src/plugins/ui_actions/public/mocks'; +import { embeddablePluginMock } from '../../../../src/plugins/embeddable/public/mocks'; +import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '.'; +import { plugin as pluginInitializer } from '.'; + +export type Setup = jest.Mocked; +export type Start = jest.Mocked; + +const createSetupContract = (): Setup => { + const setupContract: Setup = { + ...uiActionsPluginMock.createSetupContract(), + registerDrilldown: jest.fn(), + }; + return setupContract; +}; + +const createStartContract = (): Start => { + const startContract: Start = { + ...uiActionsPluginMock.createStartContract(), + getActionFactories: jest.fn(), + getActionFactory: jest.fn(), + }; + + return startContract; +}; + +const createPlugin = ( + coreSetup: CoreSetup = coreMock.createSetup(), + coreStart: CoreStart = coreMock.createStart() +) => { + const pluginInitializerContext = coreMock.createPluginInitializerContext(); + const uiActions = uiActionsPluginMock.createPlugin(); + const embeddable = embeddablePluginMock.createInstance({ + uiActions: uiActions.setup, + }); + const plugin = pluginInitializer(pluginInitializerContext); + const setup = plugin.setup(coreSetup, { + uiActions: uiActions.setup, + embeddable: embeddable.setup, + }); + + return { + pluginInitializerContext, + coreSetup, + coreStart, + plugin, + setup, + doStart: (anotherCoreStart: CoreStart = coreStart) => { + const uiActionsStart = uiActions.doStart(); + const embeddableStart = embeddable.doStart({ + uiActions: uiActionsStart, + }); + return plugin.start(anotherCoreStart, { + uiActions: uiActionsStart, + embeddable: embeddableStart, + }); + }, + }; +}; + +export const uiActionsEnhancedPluginMock = { + createSetupContract, + createStartContract, + createPlugin, +}; diff --git a/x-pack/plugins/advanced_ui_actions/public/plugin.ts b/x-pack/plugins/advanced_ui_actions/public/plugin.ts index b9f0ce43d3cdc..f042130158aec 100644 --- a/x-pack/plugins/advanced_ui_actions/public/plugin.ts +++ b/x-pack/plugins/advanced_ui_actions/public/plugin.ts @@ -11,7 +11,7 @@ import { Plugin, } from '../../../../src/core/public'; import { createReactOverlays } from '../../../../src/plugins/kibana_react/public'; -import { UiActionsStart, UiActionsSetup } from '../../../../src/plugins/ui_actions/public'; +import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, @@ -30,6 +30,7 @@ import { TimeBadgeActionContext, } from './custom_time_range_badge'; import { CommonlyUsedRange } from './types'; +import { UiActionsServiceEnhancements } from './services'; interface SetupDependencies { embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. @@ -41,8 +42,13 @@ interface StartDependencies { uiActions: UiActionsStart; } -export type Setup = void; -export type Start = void; +export interface SetupContract + extends UiActionsSetup, + Pick {} + +export interface StartContract + extends UiActionsStart, + Pick {} declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -52,12 +58,19 @@ declare module '../../../../src/plugins/ui_actions/public' { } export class AdvancedUiActionsPublicPlugin - implements Plugin { + implements Plugin { + private readonly enhancements = new UiActionsServiceEnhancements(); + constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { uiActions }: SetupDependencies): Setup {} + public setup(core: CoreSetup, { uiActions }: SetupDependencies): SetupContract { + return { + ...uiActions, + ...this.enhancements, + }; + } - public start(core: CoreStart, { uiActions }: StartDependencies): Start { + public start(core: CoreStart, { uiActions }: StartDependencies): StartContract { const dateFormat = core.uiSettings.get('dateFormat') as string; const commonlyUsedRanges = core.uiSettings.get('timepicker:quickRanges') as CommonlyUsedRange[]; const { openModal } = createReactOverlays(core); @@ -66,16 +79,19 @@ export class AdvancedUiActionsPublicPlugin dateFormat, commonlyUsedRanges, }); - uiActions.registerAction(timeRangeAction); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, timeRangeAction); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, timeRangeAction); const timeRangeBadge = new CustomTimeRangeBadge({ openModal, dateFormat, commonlyUsedRanges, }); - uiActions.registerAction(timeRangeBadge); - uiActions.attachAction(PANEL_BADGE_TRIGGER, timeRangeBadge); + uiActions.addTriggerAction(PANEL_BADGE_TRIGGER, timeRangeBadge); + + return { + ...uiActions, + ...this.enhancements, + }; } public stop() {} diff --git a/x-pack/plugins/advanced_ui_actions/public/services/index.ts b/x-pack/plugins/advanced_ui_actions/public/services/index.ts new file mode 100644 index 0000000000000..71a3429800c43 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/services/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './ui_actions_service_enhancements'; diff --git a/x-pack/plugins/advanced_ui_actions/public/services/ui_actions_service_enhancements.test.ts b/x-pack/plugins/advanced_ui_actions/public/services/ui_actions_service_enhancements.test.ts new file mode 100644 index 0000000000000..3137e35a2fe47 --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/services/ui_actions_service_enhancements.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UiActionsServiceEnhancements } from './ui_actions_service_enhancements'; +import { ActionFactoryDefinition, ActionFactory } from '../dynamic_actions'; + +describe('UiActionsService', () => { + describe('action factories', () => { + const factoryDefinition1: ActionFactoryDefinition = { + id: 'test-factory-1', + CollectConfig: {} as any, + createConfig: () => ({}), + isConfigValid: () => true, + create: () => ({} as any), + }; + const factoryDefinition2: ActionFactoryDefinition = { + id: 'test-factory-2', + CollectConfig: {} as any, + createConfig: () => ({}), + isConfigValid: () => true, + create: () => ({} as any), + }; + + test('.getActionFactories() returns empty array if no action factories registered', () => { + const service = new UiActionsServiceEnhancements(); + + const factories = service.getActionFactories(); + + expect(factories).toEqual([]); + }); + + test('can register and retrieve an action factory', () => { + const service = new UiActionsServiceEnhancements(); + + service.registerActionFactory(factoryDefinition1); + + const factory = service.getActionFactory(factoryDefinition1.id); + + expect(factory).toBeInstanceOf(ActionFactory); + expect(factory.id).toBe(factoryDefinition1.id); + }); + + test('can retrieve all action factories', () => { + const service = new UiActionsServiceEnhancements(); + + service.registerActionFactory(factoryDefinition1); + service.registerActionFactory(factoryDefinition2); + + const factories = service.getActionFactories(); + const factoriesSorted = [...factories].sort((f1, f2) => (f1.id > f2.id ? 1 : -1)); + + expect(factoriesSorted.length).toBe(2); + expect(factoriesSorted[0].id).toBe(factoryDefinition1.id); + expect(factoriesSorted[1].id).toBe(factoryDefinition2.id); + }); + + test('throws when retrieving action factory that does not exist', () => { + const service = new UiActionsServiceEnhancements(); + + service.registerActionFactory(factoryDefinition1); + + expect(() => service.getActionFactory('UNKNOWN_ID')).toThrowError( + 'Action factory [actionFactoryId = UNKNOWN_ID] does not exist.' + ); + }); + }); +}); diff --git a/x-pack/plugins/advanced_ui_actions/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/advanced_ui_actions/public/services/ui_actions_service_enhancements.ts new file mode 100644 index 0000000000000..8befbf43d3c6a --- /dev/null +++ b/x-pack/plugins/advanced_ui_actions/public/services/ui_actions_service_enhancements.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionFactoryRegistry } from '../types'; +import { ActionFactory, ActionFactoryDefinition } from '../dynamic_actions'; +import { DrilldownDefinition } from '../drilldowns'; + +export interface UiActionsServiceEnhancementsParams { + readonly actionFactories?: ActionFactoryRegistry; +} + +export class UiActionsServiceEnhancements { + protected readonly actionFactories: ActionFactoryRegistry; + + constructor({ actionFactories = new Map() }: UiActionsServiceEnhancementsParams = {}) { + this.actionFactories = actionFactories; + } + + /** + * Register an action factory. Action factories are used to configure and + * serialize/deserialize dynamic actions. + */ + public readonly registerActionFactory = < + Config extends object = object, + FactoryContext extends object = object, + ActionContext extends object = object + >( + definition: ActionFactoryDefinition + ) => { + if (this.actionFactories.has(definition.id)) { + throw new Error(`ActionFactory [actionFactory.id = ${definition.id}] already registered.`); + } + + const actionFactory = new ActionFactory(definition); + + this.actionFactories.set(actionFactory.id, actionFactory as ActionFactory); + }; + + public readonly getActionFactory = (actionFactoryId: string): ActionFactory => { + const actionFactory = this.actionFactories.get(actionFactoryId); + + if (!actionFactory) { + throw new Error(`Action factory [actionFactoryId = ${actionFactoryId}] does not exist.`); + } + + return actionFactory; + }; + + /** + * Returns an array of all action factories. + */ + public readonly getActionFactories = (): ActionFactory[] => { + return [...this.actionFactories.values()]; + }; + + /** + * Convenience method to register a {@link DrilldownDefinition | drilldown}. + */ + public readonly registerDrilldown = < + Config extends object = object, + ExecutionContext extends object = object + >({ + id: factoryId, + order, + CollectConfig, + createConfig, + isConfigValid, + getDisplayName, + euiIcon, + execute, + getHref, + }: DrilldownDefinition): void => { + const actionFactory: ActionFactoryDefinition = { + id: factoryId, + order, + CollectConfig, + createConfig, + isConfigValid, + getDisplayName, + getIconType: () => euiIcon, + isCompatible: async () => true, + create: serializedAction => ({ + id: '', + type: factoryId, + getIconType: () => euiIcon, + getDisplayName: () => serializedAction.name, + execute: async context => await execute(serializedAction.config, context), + getHref: getHref ? async context => getHref(serializedAction.config, context) : undefined, + }), + } as ActionFactoryDefinition; + + this.registerActionFactory(actionFactory); + }; +} diff --git a/x-pack/plugins/advanced_ui_actions/public/types.ts b/x-pack/plugins/advanced_ui_actions/public/types.ts index 313b09535b196..5c960192dcaff 100644 --- a/x-pack/plugins/advanced_ui_actions/public/types.ts +++ b/x-pack/plugins/advanced_ui_actions/public/types.ts @@ -5,6 +5,7 @@ */ import { KibanaReactOverlays } from '../../../../src/plugins/kibana_react/public'; +import { ActionFactory } from './dynamic_actions'; export interface CommonlyUsedRange { from: string; @@ -13,3 +14,5 @@ export interface CommonlyUsedRange { } export type OpenModal = KibanaReactOverlays['openModal']; + +export type ActionFactoryRegistry = Map; diff --git a/x-pack/plugins/dashboard_enhanced/README.md b/x-pack/plugins/dashboard_enhanced/README.md new file mode 100644 index 0000000000000..d9296ae158621 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/README.md @@ -0,0 +1 @@ +# X-Pack part of Dashboard app diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json new file mode 100644 index 0000000000000..f416ca97f7110 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "dashboardEnhanced", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["data", "advancedUiActions", "drilldowns", "embeddable", "dashboard", "share"], + "configPath": ["xpack", "dashboardEnhanced"] +} diff --git a/x-pack/plugins/dashboard_enhanced/public/index.ts b/x-pack/plugins/dashboard_enhanced/public/index.ts new file mode 100644 index 0000000000000..53540a4a1ad2e --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/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 } from 'src/core/public'; +import { DashboardEnhancedPlugin } from './plugin'; + +export { + SetupContract as DashboardEnhancedSetupContract, + SetupDependencies as DashboardEnhancedSetupDependencies, + StartContract as DashboardEnhancedStartContract, + StartDependencies as DashboardEnhancedStartDependencies, +} from './plugin'; + +export function plugin(context: PluginInitializerContext) { + return new DashboardEnhancedPlugin(context); +} diff --git a/x-pack/plugins/dashboard_enhanced/public/mocks.ts b/x-pack/plugins/dashboard_enhanced/public/mocks.ts new file mode 100644 index 0000000000000..67dc1fd97d521 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/mocks.ts @@ -0,0 +1,27 @@ +/* + * 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 { DashboardEnhancedSetupContract, DashboardEnhancedStartContract } from '.'; + +export type Setup = jest.Mocked; +export type Start = jest.Mocked; + +const createSetupContract = (): Setup => { + const setupContract: Setup = {}; + + return setupContract; +}; + +const createStartContract = (): Start => { + const startContract: Start = {}; + + return startContract; +}; + +export const dashboardEnhancedPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/x-pack/plugins/dashboard_enhanced/public/plugin.ts b/x-pack/plugins/dashboard_enhanced/public/plugin.ts new file mode 100644 index 0000000000000..772e032289bce --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/plugin.ts @@ -0,0 +1,55 @@ +/* + * 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 { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; +import { SharePluginStart, SharePluginSetup } from '../../../../src/plugins/share/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { DashboardDrilldownsService } from './services'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../advanced_ui_actions/public'; +import { DrilldownsSetup, DrilldownsStart } from '../../drilldowns/public'; + +export interface SetupDependencies { + advancedUiActions: AdvancedUiActionsSetup; + drilldowns: DrilldownsSetup; + embeddable: EmbeddableSetup; + share: SharePluginSetup; +} + +export interface StartDependencies { + advancedUiActions: AdvancedUiActionsStart; + data: DataPublicPluginStart; + drilldowns: DrilldownsStart; + embeddable: EmbeddableStart; + share: SharePluginStart; +} + +// eslint-disable-next-line +export interface SetupContract {} + +// eslint-disable-next-line +export interface StartContract {} + +export class DashboardEnhancedPlugin + implements Plugin { + public readonly drilldowns = new DashboardDrilldownsService(); + + constructor(protected readonly context: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { + this.drilldowns.bootstrap(core, plugins, { + enableDrilldowns: true, + }); + + return {}; + } + + public start(core: CoreStart, plugins: StartDependencies): StartContract { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx new file mode 100644 index 0000000000000..5ec1b881317d6 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + FlyoutCreateDrilldownAction, + OpenFlyoutAddDrilldownParams, +} from './flyout_create_drilldown'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { drilldownsPluginMock } from '../../../../../../drilldowns/public/mocks'; +import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; +import { TriggerContextMapping } from '../../../../../../../../src/plugins/ui_actions/public'; +import { MockEmbeddable, enhanceEmbeddable } from '../test_helpers'; + +const overlays = coreMock.createStart().overlays; +const drilldowns = drilldownsPluginMock.createStartContract(); + +const actionParams: OpenFlyoutAddDrilldownParams = { + start: () => ({ + core: { + overlays, + } as any, + plugins: { + drilldowns, + }, + self: {}, + }), +}; + +test('should create', () => { + expect(() => new FlyoutCreateDrilldownAction(actionParams)).not.toThrow(); +}); + +test('title is a string', () => { + expect(typeof new FlyoutCreateDrilldownAction(actionParams).getDisplayName() === 'string').toBe( + true + ); +}); + +test('icon exists', () => { + expect(typeof new FlyoutCreateDrilldownAction(actionParams).getIconType() === 'string').toBe( + true + ); +}); + +interface CompatibilityParams { + isEdit?: boolean; + isValueClickTriggerSupported?: boolean; + isEmbeddableEnhanced?: boolean; + rootType?: string; +} + +describe('isCompatible', () => { + const drilldownAction = new FlyoutCreateDrilldownAction(actionParams); + + async function assertCompatibility( + { + isEdit = true, + isValueClickTriggerSupported = true, + isEmbeddableEnhanced = true, + rootType = 'dashboard', + }: CompatibilityParams, + expectedResult: boolean = true + ): Promise { + let embeddable = new MockEmbeddable( + { id: '', viewMode: isEdit ? ViewMode.EDIT : ViewMode.VIEW }, + { + supportedTriggers: (isValueClickTriggerSupported ? ['VALUE_CLICK_TRIGGER'] : []) as Array< + keyof TriggerContextMapping + >, + } + ); + + embeddable.rootType = rootType; + + if (isEmbeddableEnhanced) { + embeddable = enhanceEmbeddable(embeddable); + } + + const result = await drilldownAction.isCompatible({ + embeddable, + }); + + expect(result).toBe(expectedResult); + } + + const assertNonCompatibility = (params: CompatibilityParams) => + assertCompatibility(params, false); + + test("compatible if dynamicUiActions enabled, 'VALUE_CLICK_TRIGGER' is supported, in edit mode", async () => { + await assertCompatibility({}); + }); + + test('not compatible if embeddable is not enhanced', async () => { + await assertNonCompatibility({ + isEmbeddableEnhanced: false, + }); + }); + + test("not compatible if 'VALUE_CLICK_TRIGGER' is not supported", async () => { + await assertNonCompatibility({ + isValueClickTriggerSupported: false, + }); + }); + + test('not compatible if in view mode', async () => { + await assertNonCompatibility({ + isEdit: false, + }); + }); + + test('not compatible if root embeddable is not "dashboard"', async () => { + await assertNonCompatibility({ + rootType: 'visualization', + }); + }); +}); + +describe('execute', () => { + const drilldownAction = new FlyoutCreateDrilldownAction(actionParams); + + test('throws error if no dynamicUiActions', async () => { + await expect( + drilldownAction.execute({ + embeddable: new MockEmbeddable({ id: '' }, {}), + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Need embeddable to be EnhancedEmbeddable to execute FlyoutCreateDrilldownAction."` + ); + }); + + test('should open flyout', async () => { + const spy = jest.spyOn(overlays, 'openFlyout'); + const embeddable = enhanceEmbeddable(new MockEmbeddable({ id: '' }, {})); + + await drilldownAction.execute({ + embeddable, + }); + + expect(spy).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx new file mode 100644 index 0000000000000..81f88e563a258 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -0,0 +1,86 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public'; +import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; +import { isEnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public'; +import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public'; +import { StartDependencies } from '../../../../plugin'; +import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public'; + +export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN'; + +export interface OpenFlyoutAddDrilldownParams { + start: StartServicesGetter>; +} + +export class FlyoutCreateDrilldownAction implements ActionByType { + public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN; + public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN; + public order = 12; + + constructor(protected readonly params: OpenFlyoutAddDrilldownParams) {} + + public getDisplayName() { + return i18n.translate('xpack.dashboard.FlyoutCreateDrilldownAction.displayName', { + defaultMessage: 'Create drilldown', + }); + } + + public getIconType() { + return 'plusInCircle'; + } + + private isEmbeddableCompatible(context: EmbeddableContext) { + if (!isEnhancedEmbeddable(context.embeddable)) return false; + const supportedTriggers = context.embeddable.supportedTriggers(); + if (!supportedTriggers || !supportedTriggers.length) return false; + if (context.embeddable.getRoot().type !== 'dashboard') return false; + + /** + * Temporarily disable drilldowns for Lens as Lens embeddable does not have + * `.embeddable` field on VALUE_CLICK_TRIGGER context. + * + * @todo Remove this condition once Lens adds `.embeddable` to field to context. + */ + if (context.embeddable.type === 'lens') return false; + + return supportedTriggers.indexOf('VALUE_CLICK_TRIGGER') > -1; + } + + public async isCompatible(context: EmbeddableContext) { + const isEditMode = context.embeddable.getInput().viewMode === 'edit'; + return isEditMode && this.isEmbeddableCompatible(context); + } + + public async execute(context: EmbeddableContext) { + const { core, plugins } = this.params.start(); + const { embeddable } = context; + + if (!isEnhancedEmbeddable(embeddable)) { + throw new Error( + 'Need embeddable to be EnhancedEmbeddable to execute FlyoutCreateDrilldownAction.' + ); + } + + const handle = core.overlays.openFlyout( + toMountPoint( + handle.close()} + placeContext={context} + viewMode={'create'} + dynamicActionManager={embeddable.enhancements.dynamicActions} + /> + ), + { + ownFocus: true, + 'data-test-subj': 'createDrilldownFlyout', + } + ); + } +} diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/index.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/index.ts new file mode 100644 index 0000000000000..4d2db209fc961 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + FlyoutCreateDrilldownAction, + OpenFlyoutAddDrilldownParams, + OPEN_FLYOUT_ADD_DRILLDOWN, +} from './flyout_create_drilldown'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx new file mode 100644 index 0000000000000..555acf1fca5ff --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx @@ -0,0 +1,148 @@ +/* + * 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 { FlyoutEditDrilldownAction, FlyoutEditDrilldownParams } from './flyout_edit_drilldown'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { drilldownsPluginMock } from '../../../../../../drilldowns/public/mocks'; +import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; +import { uiActionsEnhancedPluginMock } from '../../../../../../advanced_ui_actions/public/mocks'; +import { EnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public'; +import { MockEmbeddable, enhanceEmbeddable } from '../test_helpers'; + +const overlays = coreMock.createStart().overlays; +const drilldowns = drilldownsPluginMock.createStartContract(); +const uiActionsPlugin = uiActionsEnhancedPluginMock.createPlugin(); +const uiActions = uiActionsPlugin.doStart(); + +uiActionsPlugin.setup.registerDrilldown({ + id: 'foo', + CollectConfig: {} as any, + createConfig: () => ({}), + isConfigValid: () => true, + execute: async () => {}, + getDisplayName: () => 'test', +}); + +const actionParams: FlyoutEditDrilldownParams = { + start: () => ({ + core: { + overlays, + } as any, + plugins: { + drilldowns, + }, + self: {}, + }), +}; + +test('should create', () => { + expect(() => new FlyoutEditDrilldownAction(actionParams)).not.toThrow(); +}); + +test('title is a string', () => { + expect(typeof new FlyoutEditDrilldownAction(actionParams).getDisplayName() === 'string').toBe( + true + ); +}); + +test('icon exists', () => { + expect(typeof new FlyoutEditDrilldownAction(actionParams).getIconType() === 'string').toBe(true); +}); + +test('MenuItem exists', () => { + expect(new FlyoutEditDrilldownAction(actionParams).MenuItem).toBeDefined(); +}); + +describe('isCompatible', () => { + function setupIsCompatible({ + isEdit = true, + isEmbeddableEnhanced = true, + }: { + isEdit?: boolean; + isEmbeddableEnhanced?: boolean; + } = {}) { + const action = new FlyoutEditDrilldownAction(actionParams); + const input = { + id: '', + viewMode: isEdit ? ViewMode.EDIT : ViewMode.VIEW, + }; + const embeddable = new MockEmbeddable(input, {}); + const context = { + embeddable: (isEmbeddableEnhanced + ? enhanceEmbeddable(embeddable, uiActions) + : embeddable) as EnhancedEmbeddable, + }; + + return { + action, + context, + }; + } + + test('not compatible if no drilldowns', async () => { + const { action, context } = setupIsCompatible(); + expect(await action.isCompatible(context)).toBe(false); + }); + + test('not compatible if embeddable is not enhanced', async () => { + const { action, context } = setupIsCompatible({ isEmbeddableEnhanced: false }); + expect(await action.isCompatible(context)).toBe(false); + }); + + describe('when has at least one drilldown', () => { + test('is compatible in edit mode', async () => { + const { action, context } = setupIsCompatible(); + + await context.embeddable.enhancements.dynamicActions.createEvent( + { + config: {}, + factoryId: 'foo', + name: '', + }, + ['VALUE_CLICK_TRIGGER'] + ); + + expect(await action.isCompatible(context)).toBe(true); + }); + + test('not compatible in view mode', async () => { + const { action, context } = setupIsCompatible({ isEdit: false }); + + await context.embeddable.enhancements.dynamicActions.createEvent( + { + config: {}, + factoryId: 'foo', + name: '', + }, + ['VALUE_CLICK_TRIGGER'] + ); + + expect(await action.isCompatible(context)).toBe(false); + }); + }); +}); + +describe('execute', () => { + const drilldownAction = new FlyoutEditDrilldownAction(actionParams); + + test('throws error if no dynamicUiActions', async () => { + await expect( + drilldownAction.execute({ + embeddable: new MockEmbeddable({ id: '' }, {}), + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Need embeddable to be EnhancedEmbeddable to execute FlyoutEditDrilldownAction."` + ); + }); + + test('should open flyout', async () => { + const spy = jest.spyOn(overlays, 'openFlyout'); + await drilldownAction.execute({ + embeddable: enhanceEmbeddable(new MockEmbeddable({ id: '' }, {})), + }); + expect(spy).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx new file mode 100644 index 0000000000000..a4499ba4d757d --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -0,0 +1,74 @@ +/* + * 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 { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public'; +import { + reactToUiComponent, + toMountPoint, +} from '../../../../../../../../src/plugins/kibana_react/public'; +import { EmbeddableContext, ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; +import { txtDisplayName } from './i18n'; +import { MenuItem } from './menu_item'; +import { isEnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public'; +import { StartDependencies } from '../../../../plugin'; +import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public'; + +export const OPEN_FLYOUT_EDIT_DRILLDOWN = 'OPEN_FLYOUT_EDIT_DRILLDOWN'; + +export interface FlyoutEditDrilldownParams { + start: StartServicesGetter>; +} + +export class FlyoutEditDrilldownAction implements ActionByType { + public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN; + public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN; + public order = 10; + + constructor(protected readonly params: FlyoutEditDrilldownParams) {} + + public getDisplayName() { + return txtDisplayName; + } + + public getIconType() { + return 'list'; + } + + MenuItem = reactToUiComponent(MenuItem); + + public async isCompatible({ embeddable }: EmbeddableContext) { + if (embeddable.getInput().viewMode !== ViewMode.EDIT) return false; + if (!isEnhancedEmbeddable(embeddable)) return false; + return embeddable.enhancements.dynamicActions.state.get().events.length > 0; + } + + public async execute(context: EmbeddableContext) { + const { core, plugins } = this.params.start(); + const { embeddable } = context; + + if (!isEnhancedEmbeddable(embeddable)) { + throw new Error( + 'Need embeddable to be EnhancedEmbeddable to execute FlyoutEditDrilldownAction.' + ); + } + + const handle = core.overlays.openFlyout( + toMountPoint( + handle.close()} + placeContext={context} + viewMode={'manage'} + dynamicActionManager={embeddable.enhancements.dynamicActions} + /> + ), + { + ownFocus: true, + 'data-test-subj': 'editDrilldownFlyout', + } + ); + } +} diff --git a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/i18n.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/i18n.ts similarity index 64% rename from x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/i18n.ts rename to x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/i18n.ts index ceabc6d3a9aa5..4e2e5eb7092e4 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/i18n.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/i18n.ts @@ -6,9 +6,9 @@ import { i18n } from '@kbn/i18n'; -export const txtCreateDrilldown = i18n.translate( - 'xpack.drilldowns.components.FlyoutCreateDrilldown.CreateDrilldown', +export const txtDisplayName = i18n.translate( + 'xpack.dashboard.panel.openFlyoutEditDrilldown.displayName', { - defaultMessage: 'Create drilldown', + defaultMessage: 'Manage drilldowns', } ); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/index.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/index.tsx new file mode 100644 index 0000000000000..3e1b37f270708 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/index.tsx @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + FlyoutEditDrilldownAction, + FlyoutEditDrilldownParams, + OPEN_FLYOUT_EDIT_DRILLDOWN, +} from './flyout_edit_drilldown'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.tsx new file mode 100644 index 0000000000000..ec3a78e97eae4 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.test.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. + */ + +import React from 'react'; +import { render, cleanup, act } from '@testing-library/react/pure'; +import { MenuItem } from './menu_item'; +import { createStateContainer } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { UiActionsEnhancedDynamicActionManager as DynamicActionManager } from '../../../../../../advanced_ui_actions/public'; +import { EnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public'; +import '@testing-library/jest-dom'; + +afterEach(cleanup); + +test('', () => { + const state = createStateContainer<{ events: object[] }>({ events: [] }); + const { getByText, queryByText } = render( + + ); + + expect(getByText(/manage drilldowns/i)).toBeInTheDocument(); + expect(queryByText('0')).not.toBeInTheDocument(); + + act(() => { + state.set({ events: [{}] }); + }); + + expect(queryByText('1')).toBeInTheDocument(); +}); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.tsx new file mode 100644 index 0000000000000..5a04e03e03457 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/menu_item.tsx @@ -0,0 +1,27 @@ +/* + * 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 { EuiNotificationBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useContainerState } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { EnhancedEmbeddableContext } from '../../../../../../embeddable_enhanced/public'; +import { txtDisplayName } from './i18n'; + +export const MenuItem: React.FC<{ context: EnhancedEmbeddableContext }> = ({ context }) => { + const { events } = useContainerState(context.embeddable.enhancements.dynamicActions.state); + const count = events.length; + + return ( + + {txtDisplayName} + {count > 0 && ( + + {count} + + )} + + ); +}; diff --git a/x-pack/plugins/drilldowns/public/actions/index.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/index.ts similarity index 100% rename from x-pack/plugins/drilldowns/public/actions/index.ts rename to x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/index.ts diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts new file mode 100644 index 0000000000000..cccacf701a9ad --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/test_helpers.ts @@ -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 { Embeddable, EmbeddableInput } from '../../../../../../../src/plugins/embeddable/public'; +import { EnhancedEmbeddable } from '../../../../../embeddable_enhanced/public'; +import { + UiActionsEnhancedMemoryActionStorage as MemoryActionStorage, + UiActionsEnhancedDynamicActionManager as DynamicActionManager, + AdvancedUiActionsStart, +} from '../../../../../advanced_ui_actions/public'; +import { TriggerContextMapping } from '../../../../../../../src/plugins/ui_actions/public'; +import { uiActionsEnhancedPluginMock } from '../../../../../advanced_ui_actions/public/mocks'; + +export class MockEmbeddable extends Embeddable { + public rootType = 'dashboard'; + public readonly type = 'mock'; + private readonly triggers: Array = []; + constructor( + initialInput: EmbeddableInput, + params: { supportedTriggers?: Array } + ) { + super(initialInput, {}, undefined); + this.triggers = params.supportedTriggers ?? []; + } + public render(node: HTMLElement) {} + public reload() {} + public supportedTriggers(): Array { + return this.triggers; + } + public getRoot() { + return { + type: this.rootType, + } as Embeddable; + } +} + +export const enhanceEmbeddable = ( + embeddable: E, + uiActions: AdvancedUiActionsStart = uiActionsEnhancedPluginMock.createStartContract() +): EnhancedEmbeddable => { + (embeddable as EnhancedEmbeddable).enhancements = { + dynamicActions: new DynamicActionManager({ + storage: new MemoryActionStorage(), + isCompatible: async () => true, + uiActions, + }), + }; + return embeddable as EnhancedEmbeddable; +}; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts new file mode 100644 index 0000000000000..0161836b2c5b9 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_drilldowns_services.ts @@ -0,0 +1,57 @@ +/* + * 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 { CoreSetup } from 'src/core/public'; +import { SetupDependencies, StartDependencies } from '../../plugin'; +import { CONTEXT_MENU_TRIGGER } from '../../../../../../src/plugins/embeddable/public'; +import { EnhancedEmbeddableContext } from '../../../../embeddable_enhanced/public'; +import { + FlyoutCreateDrilldownAction, + FlyoutEditDrilldownAction, + OPEN_FLYOUT_ADD_DRILLDOWN, + OPEN_FLYOUT_EDIT_DRILLDOWN, +} from './actions'; +import { DashboardToDashboardDrilldown } from './dashboard_to_dashboard_drilldown'; +import { createStartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; + +declare module '../../../../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [OPEN_FLYOUT_ADD_DRILLDOWN]: EnhancedEmbeddableContext; + [OPEN_FLYOUT_EDIT_DRILLDOWN]: EnhancedEmbeddableContext; + } +} + +interface BootstrapParams { + enableDrilldowns: boolean; +} + +export class DashboardDrilldownsService { + bootstrap( + core: CoreSetup, + plugins: SetupDependencies, + { enableDrilldowns }: BootstrapParams + ) { + if (enableDrilldowns) { + this.setupDrilldowns(core, plugins); + } + } + + setupDrilldowns( + core: CoreSetup, + { advancedUiActions: uiActions }: SetupDependencies + ) { + const start = createStartServicesGetter(core.getStartServices); + + const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ start }); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, actionFlyoutCreateDrilldown); + + const actionFlyoutEditDrilldown = new FlyoutEditDrilldownAction({ start }); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, actionFlyoutEditDrilldown); + + const dashboardToDashboardDrilldown = new DashboardToDashboardDrilldown({ start }); + uiActions.registerDrilldown(dashboardToDashboardDrilldown); + } +} diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx new file mode 100644 index 0000000000000..dc19fccf5c92f --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx @@ -0,0 +1,164 @@ +/* + * 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 { EuiComboBoxOptionOption } from '@elastic/eui'; +import { debounce, findIndex } from 'lodash'; +import { SimpleSavedObject } from '../../../../../../../../src/core/public'; +import { DashboardDrilldownConfig } from './dashboard_drilldown_config'; +import { txtDestinationDashboardNotFound } from './i18n'; +import { CollectConfigProps } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { Config } from '../types'; +import { Params } from '../drilldown'; + +const mergeDashboards = ( + dashboards: Array>, + selectedDashboard?: EuiComboBoxOptionOption +) => { + // if we have a selected dashboard and its not in the list, append it + if (selectedDashboard && findIndex(dashboards, { value: selectedDashboard.value }) === -1) { + return [selectedDashboard, ...dashboards]; + } + return dashboards; +}; + +const dashboardSavedObjectToMenuItem = ( + savedObject: SimpleSavedObject<{ + title: string; + }> +) => ({ + value: savedObject.id, + label: savedObject.attributes.title, +}); + +interface DashboardDrilldownCollectConfigProps extends CollectConfigProps { + params: Params; +} + +interface CollectConfigContainerState { + dashboards: Array>; + searchString?: string; + isLoading: boolean; + selectedDashboard?: EuiComboBoxOptionOption; + error?: string; +} + +export class CollectConfigContainer extends React.Component< + DashboardDrilldownCollectConfigProps, + CollectConfigContainerState +> { + private isMounted = true; + state = { + dashboards: [], + isLoading: false, + searchString: undefined, + selectedDashboard: undefined, + error: undefined, + }; + + constructor(props: DashboardDrilldownCollectConfigProps) { + super(props); + this.debouncedLoadDashboards = debounce(this.loadDashboards.bind(this), 500); + } + + componentDidMount() { + this.loadSelectedDashboard(); + this.loadDashboards(); + } + + componentWillUnmount() { + this.isMounted = false; + } + + render() { + const { config, onConfig } = this.props; + const { dashboards, selectedDashboard, isLoading, error } = this.state; + + return ( + { + onConfig({ ...config, dashboardId }); + if (this.state.error) { + this.setState({ error: undefined }); + } + }} + onSearchChange={this.debouncedLoadDashboards} + onCurrentFiltersToggle={() => + onConfig({ + ...config, + useCurrentFilters: !config.useCurrentFilters, + }) + } + onKeepRangeToggle={() => + onConfig({ + ...config, + useCurrentDateRange: !config.useCurrentDateRange, + }) + } + /> + ); + } + + private async loadSelectedDashboard() { + const { + config, + params: { start }, + } = this.props; + if (!config.dashboardId) return; + const savedObject = await start().core.savedObjects.client.get<{ title: string }>( + 'dashboard', + config.dashboardId + ); + + if (!this.isMounted) return; + + // handle case when destination dashboard no longer exists + if (savedObject.error?.statusCode === 404) { + this.setState({ + error: txtDestinationDashboardNotFound(config.dashboardId), + }); + this.props.onConfig({ ...config, dashboardId: undefined }); + return; + } + + if (savedObject.error) { + this.setState({ + error: savedObject.error.message, + }); + this.props.onConfig({ ...config, dashboardId: undefined }); + return; + } + + this.setState({ selectedDashboard: dashboardSavedObjectToMenuItem(savedObject) }); + } + + private readonly debouncedLoadDashboards: (searchString?: string) => void; + private async loadDashboards(searchString?: string) { + this.setState({ searchString, isLoading: true }); + const savedObjectsClient = this.props.params.start().core.savedObjects.client; + const { savedObjects } = await savedObjectsClient.find<{ title: string }>({ + type: 'dashboard', + search: searchString ? `${searchString}*` : undefined, + searchFields: ['title^3', 'description'], + defaultSearchOperator: 'AND', + perPage: 100, + }); + + // bail out if this response is no longer needed + if (!this.isMounted) return; + if (searchString !== this.state.searchString) return; + + const dashboardList = savedObjects.map(dashboardSavedObjectToMenuItem); + + this.setState({ dashboards: dashboardList, isLoading: false }); + } +} diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx new file mode 100644 index 0000000000000..f3a966a73509c --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.story.tsx @@ -0,0 +1,63 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { DashboardDrilldownConfig } from './dashboard_drilldown_config'; + +export const dashboards = [ + { value: 'dashboard1', label: 'Dashboard 1' }, + { value: 'dashboard2', label: 'Dashboard 2' }, + { value: 'dashboard3', label: 'Dashboard 3' }, +]; + +const InteractiveDemo: React.FC = () => { + const [activeDashboardId, setActiveDashboardId] = React.useState('dashboard1'); + const [currentFilters, setCurrentFilters] = React.useState(false); + const [keepRange, setKeepRange] = React.useState(false); + + return ( + setActiveDashboardId(id)} + onCurrentFiltersToggle={() => setCurrentFilters(old => !old)} + onKeepRangeToggle={() => setKeepRange(old => !old)} + onSearchChange={() => {}} + isLoading={false} + /> + ); +}; + +storiesOf( + 'services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config', + module +) + .add('default', () => ( + console.log('onDashboardSelect', e)} + onSearchChange={() => {}} + isLoading={false} + /> + )) + .add('with switches', () => ( + console.log('onDashboardSelect', e)} + onCurrentFiltersToggle={() => console.log('onCurrentFiltersToggle')} + onKeepRangeToggle={() => console.log('onKeepRangeToggle')} + onSearchChange={() => {}} + isLoading={false} + /> + )) + .add('interactive demo', () => ); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx new file mode 100644 index 0000000000000..edeb7de48d9ac --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.test.tsx @@ -0,0 +1,10 @@ +/* + * 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. + */ + +// Need to wait for https://github.com/elastic/eui/pull/3173/ +// to unit test this component +// basic interaction is covered in end-to-end tests +test.todo(''); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx new file mode 100644 index 0000000000000..a41a5fb718219 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/dashboard_drilldown_config.tsx @@ -0,0 +1,82 @@ +/* + * 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 { EuiFormRow, EuiSwitch, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +import { + txtChooseDestinationDashboard, + txtUseCurrentFilters, + txtUseCurrentDateRange, +} from './i18n'; + +export interface DashboardDrilldownConfigProps { + activeDashboardId?: string; + dashboards: Array>; + currentFilters?: boolean; + keepRange?: boolean; + onDashboardSelect: (dashboardId: string) => void; + onCurrentFiltersToggle?: () => void; + onKeepRangeToggle?: () => void; + onSearchChange: (searchString: string) => void; + isLoading: boolean; + error?: string; +} + +export const DashboardDrilldownConfig: React.FC = ({ + activeDashboardId, + dashboards, + currentFilters, + keepRange, + onDashboardSelect, + onCurrentFiltersToggle, + onKeepRangeToggle, + onSearchChange, + isLoading, + error, +}) => { + const selectedTitle = dashboards.find(item => item.value === activeDashboardId)?.label || ''; + + return ( + <> + + + async + selectedOptions={ + activeDashboardId ? [{ label: selectedTitle, value: activeDashboardId }] : [] + } + options={dashboards} + onChange={([{ value = '' } = { value: '' }]) => onDashboardSelect(value)} + onSearchChange={onSearchChange} + isLoading={isLoading} + singleSelection={{ asPlainText: true }} + fullWidth + data-test-subj={'dashboardDrilldownSelectDashboard'} + isInvalid={!!error} + /> + + {!!onCurrentFiltersToggle && ( + + + + )} + {!!onKeepRangeToggle && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/i18n.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/i18n.ts new file mode 100644 index 0000000000000..a37f2bfa01bd4 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/i18n.ts @@ -0,0 +1,28 @@ +/* + * 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 txtChooseDestinationDashboard = i18n.translate( + 'xpack.dashboard.components.DashboardDrilldownConfig.chooseDestinationDashboard', + { + defaultMessage: 'Choose destination dashboard', + } +); + +export const txtUseCurrentFilters = i18n.translate( + 'xpack.dashboard.components.DashboardDrilldownConfig.useCurrentFilters', + { + defaultMessage: 'Use filters and query from origin dashboard', + } +); + +export const txtUseCurrentDateRange = i18n.translate( + 'xpack.dashboard.components.DashboardDrilldownConfig.useCurrentDateRange', + { + defaultMessage: 'Use date range from origin dashboard', + } +); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/index.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/index.ts new file mode 100644 index 0000000000000..b9a64a3cc17e6 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/dashboard_drilldown_config/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './dashboard_drilldown_config'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/i18n.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/i18n.ts new file mode 100644 index 0000000000000..6f6f7412f6b53 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/i18n.ts @@ -0,0 +1,16 @@ +/* + * 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 txtDestinationDashboardNotFound = (dashboardId?: string) => + i18n.translate('xpack.dashboard.drilldown.errorDestinationDashboardIsMissing', { + defaultMessage: + "Destination dashboard ('{dashboardId}') no longer exists. Choose another dashboard.", + values: { + dashboardId, + }, + }); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/index.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/index.ts new file mode 100644 index 0000000000000..c34290528d914 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { CollectConfigContainer } from './collect_config_container'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/constants.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/constants.ts new file mode 100644 index 0000000000000..e2a530b156da5 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/constants.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const DASHBOARD_TO_DASHBOARD_DRILLDOWN = 'DASHBOARD_TO_DASHBOARD_DRILLDOWN'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx new file mode 100644 index 0000000000000..18ee95cb57b3b --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx @@ -0,0 +1,363 @@ +/* + * 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 { DashboardToDashboardDrilldown } from './drilldown'; +import { UrlGeneratorContract } from '../../../../../../../src/plugins/share/public'; +import { savedObjectsServiceMock } from '../../../../../../../src/core/public/mocks'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { ActionContext, Config } from './types'; +import { + Filter, + FilterStateStore, + Query, + RangeFilter, + TimeRange, +} from '../../../../../../../src/plugins/data/common'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; + +// convenient to use real implementation here. +import { createDirectAccessDashboardLinkGenerator } from '../../../../../../../src/plugins/dashboard/public/url_generator'; +import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public'; +import { + RangeSelectTriggerContext, + ValueClickTriggerContext, +} from '../../../../../../../src/plugins/embeddable/public'; +import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; +import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public/core'; +import { StartDependencies } from '../../../plugin'; + +describe('.isConfigValid()', () => { + const drilldown = new DashboardToDashboardDrilldown({} as any); + + test('returns false for invalid config with missing dashboard id', () => { + expect( + drilldown.isConfigValid({ + dashboardId: '', + useCurrentDateRange: false, + useCurrentFilters: false, + }) + ).toBe(false); + }); + + test('returns true for valid config', () => { + expect( + drilldown.isConfigValid({ + dashboardId: 'id', + useCurrentDateRange: false, + useCurrentFilters: false, + }) + ).toBe(true); + }); +}); + +test('config component exist', () => { + const drilldown = new DashboardToDashboardDrilldown({} as any); + expect(drilldown.CollectConfig).toEqual(expect.any(Function)); +}); + +test('initial config: switches are ON', () => { + const drilldown = new DashboardToDashboardDrilldown({} as any); + const { useCurrentDateRange, useCurrentFilters } = drilldown.createConfig(); + expect(useCurrentDateRange).toBe(true); + expect(useCurrentFilters).toBe(true); +}); + +test('getHref is defined', () => { + const drilldown = new DashboardToDashboardDrilldown({} as any); + expect(drilldown.getHref).toBeDefined(); +}); + +describe('.execute() & getHref', () => { + /** + * A convenience test setup helper + * Beware: `dataPluginMock.createStartContract().actions` and extracting filters from event is mocked! + * The url generation is not mocked and uses real implementation + * So this tests are mostly focused on making sure the filters returned from `dataPluginMock.createStartContract().actions` helpers + * end up in resulting navigation path + */ + async function setupTestBed( + config: Partial, + embeddableInput: { filters?: Filter[]; timeRange?: TimeRange; query?: Query }, + filtersFromEvent: Filter[], + useRangeEvent = false + ) { + const navigateToApp = jest.fn(); + const getUrlForApp = jest.fn((app, opt) => `${app}/${opt.path}`); + const dataPluginActions = dataPluginMock.createStartContract().actions; + const savedObjectsClient = savedObjectsServiceMock.createStartContract().client; + + const drilldown = new DashboardToDashboardDrilldown({ + start: ((() => ({ + core: { + application: { + navigateToApp, + getUrlForApp, + }, + savedObjects: { + client: savedObjectsClient, + }, + }, + plugins: { + advancedUiActions: {}, + data: { + actions: dataPluginActions, + }, + share: { + urlGenerators: { + getUrlGenerator: () => + createDirectAccessDashboardLinkGenerator(() => + Promise.resolve({ + appBasePath: 'test', + useHashedUrl: false, + savedDashboardLoader: ({} as unknown) as SavedObjectLoader, + }) + ) as UrlGeneratorContract, + }, + }, + }, + self: {}, + })) as unknown) as StartServicesGetter< + Pick + >, + }); + const selectRangeFiltersSpy = jest + .spyOn(dataPluginActions, 'createFiltersFromRangeSelectAction') + .mockImplementation(() => Promise.resolve(filtersFromEvent)); + const valueClickFiltersSpy = jest + .spyOn(dataPluginActions, 'createFiltersFromValueClickAction') + .mockImplementation(() => Promise.resolve(filtersFromEvent)); + + const completeConfig: Config = { + dashboardId: 'id', + useCurrentFilters: false, + useCurrentDateRange: false, + ...config, + }; + + const context = ({ + data: useRangeEvent + ? ({ range: {} } as RangeSelectTriggerContext['data']) + : ({ data: [] } as ValueClickTriggerContext['data']), + timeFieldName: 'order_date', + embeddable: { + getInput: () => ({ + filters: [], + timeRange: { from: 'now-15m', to: 'now' }, + query: { query: 'test', language: 'kuery' }, + ...embeddableInput, + }), + }, + } as unknown) as ActionContext; + + await drilldown.execute(completeConfig, context); + + if (useRangeEvent) { + expect(selectRangeFiltersSpy).toBeCalledTimes(1); + expect(valueClickFiltersSpy).toBeCalledTimes(0); + } else { + expect(selectRangeFiltersSpy).toBeCalledTimes(0); + expect(valueClickFiltersSpy).toBeCalledTimes(1); + } + + expect(navigateToApp).toBeCalledTimes(1); + expect(navigateToApp.mock.calls[0][0]).toBe('kibana'); + + const executeNavigatedPath = navigateToApp.mock.calls[0][1]?.path; + const href = await drilldown.getHref(completeConfig, context); + + expect(href.includes(executeNavigatedPath)).toBe(true); + + return { + href, + }; + } + + test('navigates to correct dashboard', async () => { + const testDashboardId = 'dashboardId'; + const { href } = await setupTestBed( + { + dashboardId: testDashboardId, + }, + {}, + [], + false + ); + + expect(href).toEqual(expect.stringContaining(`dashboard/${testDashboardId}`)); + }); + + test('query is removed if filters are disabled', async () => { + const queryString = 'querystring'; + const queryLanguage = 'kuery'; + const { href } = await setupTestBed( + { + useCurrentFilters: false, + }, + { + query: { query: queryString, language: queryLanguage }, + }, + [] + ); + + expect(href).toEqual(expect.not.stringContaining(queryString)); + expect(href).toEqual(expect.not.stringContaining(queryLanguage)); + }); + + test('navigates with query if filters are enabled', async () => { + const queryString = 'querystring'; + const queryLanguage = 'kuery'; + const { href } = await setupTestBed( + { + useCurrentFilters: true, + }, + { + query: { query: queryString, language: queryLanguage }, + }, + [] + ); + + expect(href).toEqual(expect.stringContaining(queryString)); + expect(href).toEqual(expect.stringContaining(queryLanguage)); + }); + + test('when user chooses to keep current filters, current filters are set on destination dashboard', async () => { + const existingAppFilterKey = 'appExistingFilter'; + const existingGlobalFilterKey = 'existingGlobalFilter'; + const newAppliedFilterKey = 'newAppliedFilter'; + + const { href } = await setupTestBed( + { + useCurrentFilters: true, + }, + { + filters: [getFilter(false, existingAppFilterKey), getFilter(true, existingGlobalFilterKey)], + }, + [getFilter(false, newAppliedFilterKey)] + ); + + expect(href).toEqual(expect.stringContaining(existingAppFilterKey)); + expect(href).toEqual(expect.stringContaining(existingGlobalFilterKey)); + expect(href).toEqual(expect.stringContaining(newAppliedFilterKey)); + }); + + test('when user chooses to remove current filters, current app filters are remove on destination dashboard', async () => { + const existingAppFilterKey = 'appExistingFilter'; + const existingGlobalFilterKey = 'existingGlobalFilter'; + const newAppliedFilterKey = 'newAppliedFilter'; + + const { href } = await setupTestBed( + { + useCurrentFilters: false, + }, + { + filters: [getFilter(false, existingAppFilterKey), getFilter(true, existingGlobalFilterKey)], + }, + [getFilter(false, newAppliedFilterKey)] + ); + + expect(href).not.toEqual(expect.stringContaining(existingAppFilterKey)); + expect(href).toEqual(expect.stringContaining(existingGlobalFilterKey)); + expect(href).toEqual(expect.stringContaining(newAppliedFilterKey)); + }); + + test('when user chooses to keep current time range, current time range is passed in url', async () => { + const { href } = await setupTestBed( + { + useCurrentDateRange: true, + }, + { + timeRange: { + from: 'now-300m', + to: 'now', + }, + }, + [] + ); + + expect(href).toEqual(expect.stringContaining('now-300m')); + }); + + test('when user chooses to not keep current time range, no current time range is passed in url', async () => { + const { href } = await setupTestBed( + { + useCurrentDateRange: false, + }, + { + timeRange: { + from: 'now-300m', + to: 'now', + }, + }, + [], + false + ); + + expect(href).not.toEqual(expect.stringContaining('now-300m')); + }); + + test('if range filter contains date, then it is passed as time', async () => { + const { href } = await setupTestBed( + { + useCurrentDateRange: true, + }, + { + timeRange: { + from: 'now-300m', + to: 'now', + }, + }, + [getMockTimeRangeFilter()], + true + ); + + expect(href).not.toEqual(expect.stringContaining('now-300m')); + expect(href).toEqual(expect.stringContaining('2020-03-23')); + }); +}); + +function getFilter(isPinned: boolean, queryKey: string): Filter { + return { + $state: { + store: isPinned ? esFilters.FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE, + }, + meta: { + index: 'logstash-*', + disabled: false, + negate: false, + alias: null, + }, + query: { + match: { + [queryKey]: 'any', + }, + }, + }; +} + +function getMockTimeRangeFilter(): RangeFilter { + return { + meta: { + index: 'logstash-*', + params: { + gte: '2020-03-23T13:10:29.665Z', + lt: '2020-03-23T13:10:36.736Z', + format: 'strict_date_optional_time', + }, + type: 'range', + key: 'order_date', + disabled: false, + negate: false, + alias: null, + }, + range: { + order_date: { + gte: '2020-03-23T13:10:29.665Z', + lt: '2020-03-23T13:10:36.736Z', + format: 'strict_date_optional_time', + }, + }, + }; +} diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx new file mode 100644 index 0000000000000..848e77384f7f0 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; +import { DASHBOARD_APP_URL_GENERATOR } from '../../../../../../../src/plugins/dashboard/public'; +import { ActionContext, Config } from './types'; +import { CollectConfigContainer } from './components'; +import { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants'; +import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../advanced_ui_actions/public'; +import { txtGoToDashboard } from './i18n'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; +import { VisualizeEmbeddableContract } from '../../../../../../../src/plugins/visualizations/public'; +import { + isRangeSelectTriggerContext, + isValueClickTriggerContext, +} from '../../../../../../../src/plugins/embeddable/public'; +import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public'; +import { StartDependencies } from '../../../plugin'; + +export interface Params { + start: StartServicesGetter>; +} + +export class DashboardToDashboardDrilldown + implements Drilldown> { + constructor(protected readonly params: Params) {} + + public readonly id = DASHBOARD_TO_DASHBOARD_DRILLDOWN; + + public readonly order = 100; + + public readonly getDisplayName = () => txtGoToDashboard; + + public readonly euiIcon = 'dashboardApp'; + + private readonly ReactCollectConfig: React.FC = props => ( + + ); + + public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); + + public readonly createConfig = () => ({ + dashboardId: '', + useCurrentFilters: true, + useCurrentDateRange: true, + }); + + public readonly isConfigValid = (config: Config): config is Config => { + if (!config.dashboardId) return false; + return true; + }; + + public readonly getHref = async ( + config: Config, + context: ActionContext + ): Promise => { + const dashboardPath = await this.getDestinationUrl(config, context); + const dashboardHash = dashboardPath.split('#')[1]; + + return this.params.start().core.application.getUrlForApp('kibana', { + path: `#${dashboardHash}`, + }); + }; + + public readonly execute = async ( + config: Config, + context: ActionContext + ) => { + const dashboardPath = await this.getDestinationUrl(config, context); + const dashboardHash = dashboardPath.split('#')[1]; + + await this.params.start().core.application.navigateToApp('kibana', { + path: `#${dashboardHash}`, + }); + }; + + private getDestinationUrl = async ( + config: Config, + context: ActionContext + ): Promise => { + const { + createFiltersFromRangeSelectAction, + createFiltersFromValueClickAction, + } = this.params.start().plugins.data.actions; + const { + timeRange: currentTimeRange, + query, + filters: currentFilters, + } = context.embeddable!.getInput(); + + // if useCurrentDashboardFilters enabled, then preserve all the filters (pinned and unpinned) + // otherwise preserve only pinned + const existingFilters = + (config.useCurrentFilters + ? currentFilters + : currentFilters?.filter(f => esFilters.isFilterPinned(f))) ?? []; + + // if useCurrentDashboardDataRange is enabled, then preserve current time range + // if undefined is passed, then destination dashboard will figure out time range itself + // for brush event this time range would be overwritten + let timeRange = config.useCurrentDateRange ? currentTimeRange : undefined; + let filtersFromEvent = await (async () => { + try { + if (isRangeSelectTriggerContext(context)) + return await createFiltersFromRangeSelectAction(context.data); + if (isValueClickTriggerContext(context)) + return await createFiltersFromValueClickAction(context.data); + + // eslint-disable-next-line no-console + console.warn( + ` + DashboardToDashboard drilldown: can't extract filters from action. + Is it not supported action?`, + context + ); + + return []; + } catch (e) { + // eslint-disable-next-line no-console + console.warn( + ` + DashboardToDashboard drilldown: error extracting filters from action. + Continuing without applying filters from event`, + e + ); + return []; + } + })(); + + if (context.timeFieldName) { + const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( + context.timeFieldName, + filtersFromEvent + ); + filtersFromEvent = restOfFilters; + if (timeRangeFilter) { + timeRange = esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter); + } + } + + const { plugins } = this.params.start(); + + return plugins.share.urlGenerators.getUrlGenerator(DASHBOARD_APP_URL_GENERATOR).createUrl({ + dashboardId: config.dashboardId, + query: config.useCurrentFilters ? query : undefined, + timeRange, + filters: [...existingFilters, ...filtersFromEvent], + }); + }; +} diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/i18n.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/i18n.ts new file mode 100644 index 0000000000000..98b746bafd24a --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/i18n.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtGoToDashboard = i18n.translate('xpack.dashboard.drilldown.goToDashboard', { + defaultMessage: 'Go to Dashboard', +}); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/index.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/index.ts new file mode 100644 index 0000000000000..914f34980a272 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/index.ts @@ -0,0 +1,15 @@ +/* + * 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 { DASHBOARD_TO_DASHBOARD_DRILLDOWN } from './constants'; +export { + DashboardToDashboardDrilldown, + Params as DashboardToDashboardDrilldownParams, +} from './drilldown'; +export { + ActionContext as DashboardToDashboardActionContext, + Config as DashboardToDashboardConfig, +} from './types'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts new file mode 100644 index 0000000000000..1fbff0a7269e2 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/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 { + ValueClickTriggerContext, + RangeSelectTriggerContext, + IEmbeddable, +} from '../../../../../../../src/plugins/embeddable/public'; + +export type ActionContext = + | ValueClickTriggerContext + | RangeSelectTriggerContext; + +export interface Config { + dashboardId?: string; + useCurrentFilters: boolean; + useCurrentDateRange: boolean; +} diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/index.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/index.ts new file mode 100644 index 0000000000000..7be8f1c65da12 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './dashboard_drilldowns_services'; diff --git a/x-pack/plugins/drilldowns/public/service/index.ts b/x-pack/plugins/dashboard_enhanced/public/services/index.ts similarity index 86% rename from x-pack/plugins/drilldowns/public/service/index.ts rename to x-pack/plugins/dashboard_enhanced/public/services/index.ts index 44472b18a5317..8cc3e12906531 100644 --- a/x-pack/plugins/drilldowns/public/service/index.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './drilldown_service'; +export * from './drilldowns'; diff --git a/x-pack/plugins/dashboard_enhanced/scripts/storybook.js b/x-pack/plugins/dashboard_enhanced/scripts/storybook.js new file mode 100644 index 0000000000000..5d95c56c31e3b --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/scripts/storybook.js @@ -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 { join } from 'path'; + +// eslint-disable-next-line +require('@kbn/storybook').runStorybookCli({ + name: 'dashboard_enhanced', + storyGlobs: [join(__dirname, '..', 'public', '**', '*.story.tsx')], +}); diff --git a/x-pack/plugins/drilldowns/kibana.json b/x-pack/plugins/drilldowns/kibana.json index 4dba07b5a7be3..678c054aa322c 100644 --- a/x-pack/plugins/drilldowns/kibana.json +++ b/x-pack/plugins/drilldowns/kibana.json @@ -3,9 +3,6 @@ "version": "kibana", "server": false, "ui": true, - "configPath": ["xpack", "drilldowns"], - "requiredPlugins": [ - "uiActions", - "embeddable" - ] + "requiredPlugins": ["uiActions", "embeddable", "advancedUiActions"], + "configPath": ["xpack", "drilldowns"] } diff --git a/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx b/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx deleted file mode 100644 index 4834cc8081374..0000000000000 --- a/x-pack/plugins/drilldowns/public/actions/flyout_create_drilldown/index.tsx +++ /dev/null @@ -1,52 +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 { i18n } from '@kbn/i18n'; -import { CoreStart } from 'src/core/public'; -import { ActionByType } from '../../../../../../src/plugins/ui_actions/public'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; -import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; -import { FlyoutCreateDrilldown } from '../../components/flyout_create_drilldown'; - -export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN'; - -export interface FlyoutCreateDrilldownActionContext { - embeddable: IEmbeddable; -} - -export interface OpenFlyoutAddDrilldownParams { - overlays: () => Promise; -} - -export class FlyoutCreateDrilldownAction implements ActionByType { - public readonly type = OPEN_FLYOUT_ADD_DRILLDOWN; - public readonly id = OPEN_FLYOUT_ADD_DRILLDOWN; - public order = 100; - - constructor(protected readonly params: OpenFlyoutAddDrilldownParams) {} - - public getDisplayName() { - return i18n.translate('xpack.drilldowns.FlyoutCreateDrilldownAction.displayName', { - defaultMessage: 'Create drilldown', - }); - } - - public getIconType() { - return 'plusInCircle'; - } - - public async isCompatible({ embeddable }: FlyoutCreateDrilldownActionContext) { - return embeddable.getInput().viewMode === 'edit'; - } - - public async execute(context: FlyoutCreateDrilldownActionContext) { - const overlays = await this.params.overlays(); - const handle = overlays.openFlyout( - toMountPoint( handle.close()} />) - ); - } -} diff --git a/x-pack/plugins/drilldowns/public/actions/flyout_edit_drilldown/index.tsx b/x-pack/plugins/drilldowns/public/actions/flyout_edit_drilldown/index.tsx deleted file mode 100644 index f109da94fcaca..0000000000000 --- a/x-pack/plugins/drilldowns/public/actions/flyout_edit_drilldown/index.tsx +++ /dev/null @@ -1,72 +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 { i18n } from '@kbn/i18n'; -import { CoreStart } from 'src/core/public'; -import { EuiNotificationBadge } from '@elastic/eui'; -import { ActionByType } from '../../../../../../src/plugins/ui_actions/public'; -import { - toMountPoint, - reactToUiComponent, -} from '../../../../../../src/plugins/kibana_react/public'; -import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; -import { FormCreateDrilldown } from '../../components/form_create_drilldown'; - -export const OPEN_FLYOUT_EDIT_DRILLDOWN = 'OPEN_FLYOUT_EDIT_DRILLDOWN'; - -export interface FlyoutEditDrilldownActionContext { - embeddable: IEmbeddable; -} - -export interface FlyoutEditDrilldownParams { - overlays: () => Promise; -} - -const displayName = i18n.translate('xpack.drilldowns.panel.openFlyoutEditDrilldown.displayName', { - defaultMessage: 'Manage drilldowns', -}); - -// mocked data -const drilldrownCount = 2; - -export class FlyoutEditDrilldownAction implements ActionByType { - public readonly type = OPEN_FLYOUT_EDIT_DRILLDOWN; - public readonly id = OPEN_FLYOUT_EDIT_DRILLDOWN; - public order = 100; - - constructor(protected readonly params: FlyoutEditDrilldownParams) {} - - public getDisplayName() { - return displayName; - } - - public getIconType() { - return 'list'; - } - - private ReactComp: React.FC<{ context: FlyoutEditDrilldownActionContext }> = () => { - return ( - <> - {displayName}{' '} - - {drilldrownCount} - - - ); - }; - - MenuItem = reactToUiComponent(this.ReactComp); - - public async isCompatible({ embeddable }: FlyoutEditDrilldownActionContext) { - return embeddable.getInput().viewMode === 'edit' && drilldrownCount > 0; - } - - public async execute({ embeddable }: FlyoutEditDrilldownActionContext) { - const overlays = await this.params.overlays(); - overlays.openFlyout(toMountPoint()); - } -} diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx new file mode 100644 index 0000000000000..16b4d3a25d9e5 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx @@ -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 * as React from 'react'; +import { EuiFlyout } from '@elastic/eui'; +import { storiesOf } from '@storybook/react'; +import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns'; +import { + dashboardFactory, + urlFactory, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../advanced_ui_actions/public/components/action_wizard/test_data'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { StubBrowserStorage } from '../../../../../../src/test_utils/public/stub_browser_storage'; +import { mockDynamicActionManager } from './test_data'; + +const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({ + advancedUiActions: { + getActionFactories() { + return [dashboardFactory, urlFactory]; + }, + } as any, + storage: new Storage(new StubBrowserStorage()), + notifications: { + toasts: { + addError: (...args: any[]) => { + alert(JSON.stringify(args)); + }, + addSuccess: (...args: any[]) => { + alert(JSON.stringify(args)); + }, + } as any, + }, +}); + +storiesOf('components/FlyoutManageDrilldowns', module).add('default', () => ( + {}}> + + +)); diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx new file mode 100644 index 0000000000000..6749b41e81fc7 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx @@ -0,0 +1,221 @@ +/* + * 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 { cleanup, fireEvent, render, wait } from '@testing-library/react/pure'; +import '@testing-library/jest-dom/extend-expect'; +import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns'; +import { + dashboardFactory, + urlFactory, +} from '../../../../advanced_ui_actions/public/components/action_wizard/test_data'; +import { StubBrowserStorage } from '../../../../../../src/test_utils/public/stub_browser_storage'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { mockDynamicActionManager } from './test_data'; +import { TEST_SUBJ_DRILLDOWN_ITEM } from '../list_manage_drilldowns'; +import { WELCOME_MESSAGE_TEST_SUBJ } from '../drilldown_hello_bar'; +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { NotificationsStart } from 'kibana/public'; +import { toastDrilldownsCRUDError } from './i18n'; + +const storage = new Storage(new StubBrowserStorage()); +const notifications = coreMock.createStart().notifications; +const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({ + advancedUiActions: { + getActionFactories() { + return [dashboardFactory, urlFactory]; + }, + } as any, + storage, + notifications, +}); + +// https://github.com/elastic/kibana/issues/59469 +afterEach(cleanup); + +beforeEach(() => { + storage.clear(); + (notifications.toasts as jest.Mocked).addSuccess.mockClear(); + (notifications.toasts as jest.Mocked).addError.mockClear(); +}); + +test('Allows to manage drilldowns', async () => { + const screen = render( + + ); + + // wait for initial render. It is async because resolving compatible action factories is async + await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + + // no drilldowns in the list + expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0); + + fireEvent.click(screen.getByText(/Create new/i)); + + let [createHeading, createButton] = screen.getAllByText(/Create Drilldown/i); + expect(createHeading).toBeVisible(); + expect(screen.getByLabelText(/Back/i)).toBeVisible(); + + expect(createButton).toBeDisabled(); + + // input drilldown name + const name = 'Test name'; + fireEvent.change(screen.getByLabelText(/name/i), { + target: { value: name }, + }); + + // select URL one + fireEvent.click(screen.getByText(/Go to URL/i)); + + // Input url + const URL = 'https://elastic.co'; + fireEvent.change(screen.getByLabelText(/url/i), { + target: { value: URL }, + }); + + [createHeading, createButton] = screen.getAllByText(/Create Drilldown/i); + + expect(createButton).toBeEnabled(); + fireEvent.click(createButton); + + expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible(); + + await wait(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(1)); + expect(screen.getByText(name)).toBeVisible(); + const editButton = screen.getByText(/edit/i); + fireEvent.click(editButton); + + expect(screen.getByText(/Edit Drilldown/i)).toBeVisible(); + // check that wizard is prefilled with current drilldown values + expect(screen.getByLabelText(/name/i)).toHaveValue(name); + expect(screen.getByLabelText(/url/i)).toHaveValue(URL); + + // input new drilldown name + const newName = 'New drilldown name'; + fireEvent.change(screen.getByLabelText(/name/i), { + target: { value: newName }, + }); + fireEvent.click(screen.getByText(/save/i)); + + expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible(); + await wait(() => screen.getByText(newName)); + + // delete drilldown from edit view + fireEvent.click(screen.getByText(/edit/i)); + fireEvent.click(screen.getByText(/delete/i)); + + expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible(); + await wait(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0)); +}); + +test('Can delete multiple drilldowns', async () => { + const screen = render( + + ); + // wait for initial render. It is async because resolving compatible action factories is async + await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + + const createDrilldown = async () => { + const oldCount = screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM).length; + fireEvent.click(screen.getByText(/Create new/i)); + fireEvent.change(screen.getByLabelText(/name/i), { + target: { value: 'test' }, + }); + fireEvent.click(screen.getByText(/Go to URL/i)); + fireEvent.change(screen.getByLabelText(/url/i), { + target: { value: 'https://elastic.co' }, + }); + fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); + await wait(() => + expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(oldCount + 1) + ); + }; + + await createDrilldown(); + await createDrilldown(); + await createDrilldown(); + + const checkboxes = screen.getAllByLabelText(/Select this drilldown/i); + expect(checkboxes).toHaveLength(3); + checkboxes.forEach(checkbox => fireEvent.click(checkbox)); + expect(screen.queryByText(/Create/i)).not.toBeInTheDocument(); + fireEvent.click(screen.getByText(/Delete \(3\)/i)); + + await wait(() => expect(screen.queryAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(0)); +}); + +test('Create only mode', async () => { + const onClose = jest.fn(); + const screen = render( + + ); + // wait for initial render. It is async because resolving compatible action factories is async + await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + fireEvent.change(screen.getByLabelText(/name/i), { + target: { value: 'test' }, + }); + fireEvent.click(screen.getByText(/Go to URL/i)); + fireEvent.change(screen.getByLabelText(/url/i), { + target: { value: 'https://elastic.co' }, + }); + fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); + + await wait(() => expect(notifications.toasts.addSuccess).toBeCalled()); + expect(onClose).toBeCalled(); + expect(await mockDynamicActionManager.state.get().events.length).toBe(1); +}); + +test.todo("Error when can't fetch drilldown list"); + +test("Error when can't save drilldown changes", async () => { + const error = new Error('Oops'); + jest.spyOn(mockDynamicActionManager, 'createEvent').mockImplementationOnce(async () => { + throw error; + }); + const screen = render( + + ); + // wait for initial render. It is async because resolving compatible action factories is async + await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + fireEvent.click(screen.getByText(/Create new/i)); + fireEvent.change(screen.getByLabelText(/name/i), { + target: { value: 'test' }, + }); + fireEvent.click(screen.getByText(/Go to URL/i)); + fireEvent.change(screen.getByLabelText(/url/i), { + target: { value: 'https://elastic.co' }, + }); + fireEvent.click(screen.getAllByText(/Create Drilldown/i)[1]); + await wait(() => + expect(notifications.toasts.addError).toBeCalledWith(error, { title: toastDrilldownsCRUDError }) + ); +}); + +test('Should show drilldown welcome message. Should be able to dismiss it', async () => { + let screen = render( + + ); + + // wait for initial render. It is async because resolving compatible action factories is async + await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + + expect(screen.getByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeVisible(); + fireEvent.click(screen.getByText(/hide/i)); + expect(screen.queryByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeNull(); + cleanup(); + + screen = render( + + ); + // wait for initial render. It is async because resolving compatible action factories is async + await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); + expect(screen.queryByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeNull(); +}); diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx new file mode 100644 index 0000000000000..0d4a67e325e4d --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -0,0 +1,331 @@ +/* + * 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, { useEffect, useState } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; +import { + AdvancedUiActionsActionFactory as ActionFactory, + AdvancedUiActionsStart, + UiActionsEnhancedDynamicActionManager as DynamicActionManager, + UiActionsEnhancedSerializedAction, + UiActionsEnhancedSerializedEvent, +} from '../../../../advanced_ui_actions/public'; +import { NotificationsStart } from '../../../../../../src/core/public'; +import { DrilldownWizardConfig, FlyoutDrilldownWizard } from '../flyout_drilldown_wizard'; +import { FlyoutListManageDrilldowns } from '../flyout_list_manage_drilldowns'; +import { IStorageWrapper } from '../../../../../../src/plugins/kibana_utils/public'; +import { + VALUE_CLICK_TRIGGER, + SELECT_RANGE_TRIGGER, + TriggerContextMapping, +} from '../../../../../../src/plugins/ui_actions/public'; +import { useContainerState } from '../../../../../../src/plugins/kibana_utils/public'; +import { DrilldownListItem } from '../list_manage_drilldowns'; +import { + toastDrilldownCreated, + toastDrilldownDeleted, + toastDrilldownEdited, + toastDrilldownsCRUDError, + toastDrilldownsDeleted, +} from './i18n'; + +interface ConnectedFlyoutManageDrilldownsProps { + placeContext: Context; + dynamicActionManager: DynamicActionManager; + viewMode?: 'create' | 'manage'; + onClose?: () => void; +} + +/** + * Represent current state (route) of FlyoutManageDrilldowns + */ +enum Routes { + Manage = 'manage', + Create = 'create', + Edit = 'edit', +} + +export function createFlyoutManageDrilldowns({ + advancedUiActions, + storage, + notifications, +}: { + advancedUiActions: AdvancedUiActionsStart; + storage: IStorageWrapper; + notifications: NotificationsStart; +}) { + // fine to assume this is static, + // because all action factories should be registered in setup phase + const allActionFactories = advancedUiActions.getActionFactories(); + const allActionFactoriesById = allActionFactories.reduce((acc, next) => { + acc[next.id] = next; + return acc; + }, {} as Record); + + return (props: ConnectedFlyoutManageDrilldownsProps) => { + const isCreateOnly = props.viewMode === 'create'; + + const selectedTriggers: Array = React.useMemo( + () => [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER], + [] + ); + + const factoryContext: object = React.useMemo( + () => ({ + placeContext: props.placeContext, + triggers: selectedTriggers, + }), + [props.placeContext, selectedTriggers] + ); + + const actionFactories = useCompatibleActionFactoriesForCurrentContext( + allActionFactories, + factoryContext + ); + + const [route, setRoute] = useState( + () => (isCreateOnly ? Routes.Create : Routes.Manage) // initial state is different depending on `viewMode` + ); + const [currentEditId, setCurrentEditId] = useState(null); + + const [shouldShowWelcomeMessage, onHideWelcomeMessage] = useWelcomeMessage(storage); + + const { + drilldowns, + createDrilldown, + editDrilldown, + deleteDrilldown, + } = useDrilldownsStateManager(props.dynamicActionManager, notifications); + + /** + * isCompatible promise is not yet resolved. + * Skip rendering until it is resolved + */ + if (!actionFactories) return null; + /** + * Drilldowns are not fetched yet or error happened during fetching + * In case of error user is notified with toast + */ + if (!drilldowns) return null; + + /** + * Needed for edit mode to prefill wizard fields with data from current edited drilldown + */ + function resolveInitialDrilldownWizardConfig(): DrilldownWizardConfig | undefined { + if (route !== Routes.Edit) return undefined; + if (!currentEditId) return undefined; + const drilldownToEdit = drilldowns?.find(d => d.eventId === currentEditId); + if (!drilldownToEdit) return undefined; + + return { + actionFactory: allActionFactoriesById[drilldownToEdit.action.factoryId], + actionConfig: drilldownToEdit.action.config as object, + name: drilldownToEdit.action.name, + }; + } + + /** + * Maps drilldown to list item view model + */ + function mapToDrilldownToDrilldownListItem( + drilldown: UiActionsEnhancedSerializedEvent + ): DrilldownListItem { + const actionFactory = allActionFactoriesById[drilldown.action.factoryId]; + return { + id: drilldown.eventId, + drilldownName: drilldown.action.name, + actionName: actionFactory?.getDisplayName(factoryContext) ?? drilldown.action.factoryId, + icon: actionFactory?.getIconType(factoryContext), + }; + } + + switch (route) { + case Routes.Create: + case Routes.Edit: + return ( + setRoute(Routes.Manage)} + onSubmit={({ actionConfig, actionFactory, name }) => { + if (route === Routes.Create) { + createDrilldown( + { + name, + config: actionConfig, + factoryId: actionFactory.id, + }, + selectedTriggers + ); + } else { + editDrilldown( + currentEditId!, + { + name, + config: actionConfig, + factoryId: actionFactory.id, + }, + selectedTriggers + ); + } + + if (isCreateOnly) { + if (props.onClose) { + props.onClose(); + } + } else { + setRoute(Routes.Manage); + } + + setCurrentEditId(null); + }} + onDelete={() => { + deleteDrilldown(currentEditId!); + setRoute(Routes.Manage); + setCurrentEditId(null); + }} + actionFactoryContext={factoryContext} + initialDrilldownWizardConfig={resolveInitialDrilldownWizardConfig()} + /> + ); + + case Routes.Manage: + default: + return ( + { + setCurrentEditId(null); + deleteDrilldown(ids); + }} + onEdit={id => { + setCurrentEditId(id); + setRoute(Routes.Edit); + }} + onCreate={() => { + setCurrentEditId(null); + setRoute(Routes.Create); + }} + onClose={props.onClose} + /> + ); + } + }; +} + +function useCompatibleActionFactoriesForCurrentContext( + actionFactories: Array>, + context: Context +) { + const [compatibleActionFactories, setCompatibleActionFactories] = useState< + Array> + >(); + useEffect(() => { + let canceled = false; + async function updateCompatibleFactoriesForContext() { + const compatibility = await Promise.all( + actionFactories.map(factory => factory.isCompatible(context)) + ); + if (canceled) return; + setCompatibleActionFactories(actionFactories.filter((_, i) => compatibility[i])); + } + updateCompatibleFactoriesForContext(); + return () => { + canceled = true; + }; + }, [context, actionFactories]); + + return compatibleActionFactories; +} + +function useWelcomeMessage(storage: IStorageWrapper): [boolean, () => void] { + const key = `drilldowns:hidWelcomeMessage`; + const [hidWelcomeMessage, setHidWelcomeMessage] = useState(storage.get(key) ?? false); + + return [ + !hidWelcomeMessage, + () => { + if (hidWelcomeMessage) return; + setHidWelcomeMessage(true); + storage.set(key, true); + }, + ]; +} + +function useDrilldownsStateManager( + actionManager: DynamicActionManager, + notifications: NotificationsStart +) { + const { events: drilldowns } = useContainerState(actionManager.state); + const [isLoading, setIsLoading] = useState(false); + const isMounted = useMountedState(); + + async function run(op: () => Promise) { + setIsLoading(true); + try { + await op(); + } catch (e) { + notifications.toasts.addError(e, { + title: toastDrilldownsCRUDError, + }); + if (!isMounted) return; + setIsLoading(false); + return; + } + } + + async function createDrilldown( + action: UiActionsEnhancedSerializedAction, + selectedTriggers: Array + ) { + await run(async () => { + await actionManager.createEvent(action, selectedTriggers); + notifications.toasts.addSuccess({ + title: toastDrilldownCreated.title, + text: toastDrilldownCreated.text(action.name), + }); + }); + } + + async function editDrilldown( + drilldownId: string, + action: UiActionsEnhancedSerializedAction, + selectedTriggers: Array + ) { + await run(async () => { + await actionManager.updateEvent(drilldownId, action, selectedTriggers); + notifications.toasts.addSuccess({ + title: toastDrilldownEdited.title, + text: toastDrilldownEdited.text(action.name), + }); + }); + } + + async function deleteDrilldown(drilldownIds: string | string[]) { + await run(async () => { + drilldownIds = Array.isArray(drilldownIds) ? drilldownIds : [drilldownIds]; + await actionManager.deleteEvents(drilldownIds); + notifications.toasts.addSuccess( + drilldownIds.length === 1 + ? { + title: toastDrilldownDeleted.title, + text: toastDrilldownDeleted.text, + } + : { + title: toastDrilldownsDeleted.title, + text: toastDrilldownsDeleted.text(drilldownIds.length), + } + ); + }); + } + + return { drilldowns, isLoading, createDrilldown, editDrilldown, deleteDrilldown }; +} diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/i18n.ts b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/i18n.ts new file mode 100644 index 0000000000000..31384860786ef --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/i18n.ts @@ -0,0 +1,88 @@ +/* + * 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 toastDrilldownCreated = { + title: i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedTitle', + { + defaultMessage: 'Drilldown created', + } + ), + text: (drilldownName: string) => + i18n.translate('xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownCreatedText', { + defaultMessage: 'You created "{drilldownName}". Save dashboard before testing.', + values: { + drilldownName, + }, + }), +}; + +export const toastDrilldownEdited = { + title: i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedTitle', + { + defaultMessage: 'Drilldown edited', + } + ), + text: (drilldownName: string) => + i18n.translate('xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownEditedText', { + defaultMessage: 'You edited "{drilldownName}". Save dashboard before testing.', + values: { + drilldownName, + }, + }), +}; + +export const toastDrilldownDeleted = { + title: i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedTitle', + { + defaultMessage: 'Drilldown deleted', + } + ), + text: i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownDeletedText', + { + defaultMessage: 'You deleted a drilldown.', + } + ), +}; + +export const toastDrilldownsDeleted = { + title: i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedTitle', + { + defaultMessage: 'Drilldowns deleted', + } + ), + text: (n: number) => + i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsDeletedText', + { + defaultMessage: 'You deleted {n} drilldowns', + values: { + n, + }, + } + ), +}; + +export const toastDrilldownsCRUDError = i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsCRUDErrorTitle', + { + defaultMessage: 'Error saving drilldown', + description: 'Title for generic error toast when persisting drilldown updates failed', + } +); + +export const toastDrilldownsFetchError = i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.toast.drilldownsFetchErrorTitle', + { + defaultMessage: 'Error fetching drilldowns', + } +); diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/index.ts b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/index.ts new file mode 100644 index 0000000000000..f084a3e563c23 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './connected_flyout_manage_drilldowns'; diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/test_data.ts b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/test_data.ts new file mode 100644 index 0000000000000..47a04222286cb --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/test_data.ts @@ -0,0 +1,89 @@ +/* + * 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 uuid from 'uuid'; +import { + UiActionsEnhancedDynamicActionManager as DynamicActionManager, + UiActionsEnhancedDynamicActionManagerState as DynamicActionManagerState, + UiActionsEnhancedSerializedAction, +} from '../../../../advanced_ui_actions/public'; +import { TriggerContextMapping } from '../../../../../../src/plugins/ui_actions/public'; +import { createStateContainer } from '../../../../../../src/plugins/kibana_utils/common'; + +class MockDynamicActionManager implements PublicMethodsOf { + public readonly state = createStateContainer({ + isFetchingEvents: false, + fetchCount: 0, + events: [], + }); + + async count() { + return this.state.get().events.length; + } + + async list() { + return this.state.get().events; + } + + async createEvent( + action: UiActionsEnhancedSerializedAction, + triggers: Array + ) { + const event = { + action, + triggers, + eventId: uuid(), + }; + const state = this.state.get(); + this.state.set({ + ...state, + events: [...state.events, event], + }); + } + + async deleteEvents(eventIds: string[]) { + const state = this.state.get(); + let events = state.events; + + eventIds.forEach(id => { + events = events.filter(e => e.eventId !== id); + }); + + this.state.set({ + ...state, + events, + }); + } + + async updateEvent( + eventId: string, + action: UiActionsEnhancedSerializedAction, + triggers: Array + ) { + const state = this.state.get(); + const events = state.events; + const idx = events.findIndex(e => e.eventId === eventId); + const event = { + eventId, + action, + triggers, + }; + + this.state.set({ + ...state, + events: [...events.slice(0, idx), event, ...events.slice(idx + 1)], + }); + } + + async deleteEvent() { + throw new Error('not implemented'); + } + + async start() {} + async stop() {} +} + +export const mockDynamicActionManager = (new MockDynamicActionManager() as unknown) as DynamicActionManager; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.story.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.story.tsx index 7a9e19342f27c..c4a4630397f1c 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.story.tsx +++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.story.tsx @@ -8,6 +8,16 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; import { DrilldownHelloBar } from '.'; -storiesOf('components/DrilldownHelloBar', module).add('default', () => { - return ; -}); +const Demo = () => { + const [show, setShow] = React.useState(true); + return show ? ( + { + setShow(false); + }} + /> + ) : null; +}; + +storiesOf('components/DrilldownHelloBar', module).add('default', () => ); 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 index 1ef714f7b86e2..48e17dadc810f 100644 --- 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 @@ -5,22 +5,58 @@ */ import React from 'react'; +import { + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiTextColor, + EuiText, + EuiLink, + EuiSpacer, + EuiButtonEmpty, + EuiIcon, +} from '@elastic/eui'; +import { txtHideHelpButtonLabel, txtHelpText, txtViewDocsLinkLabel } from './i18n'; export interface DrilldownHelloBarProps { docsLink?: string; + onHideClick?: () => void; } -/** - * @todo https://github.com/elastic/kibana/issues/55311 - */ -export const DrilldownHelloBar: React.FC = ({ docsLink }) => { +export const WELCOME_MESSAGE_TEST_SUBJ = 'drilldownsWelcomeMessage'; + +export const DrilldownHelloBar: React.FC = ({ + docsLink, + onHideClick = () => {}, +}) => { 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 -
    + + +
    + +
    +
    + + + {txtHelpText} + + {docsLink && ( + <> + + {txtViewDocsLinkLabel} + + )} + + + + {txtHideHelpButtonLabel} + + + + } + /> ); }; diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/i18n.ts b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/i18n.ts new file mode 100644 index 0000000000000..63dc95dabc0fb --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/i18n.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const txtHelpText = i18n.translate( + 'xpack.drilldowns.components.DrilldownHelloBar.helpText', + { + defaultMessage: + '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.', + } +); + +export const txtViewDocsLinkLabel = i18n.translate( + 'xpack.drilldowns.components.DrilldownHelloBar.viewDocsLinkLabel', + { + defaultMessage: 'View docs', + } +); + +export const txtHideHelpButtonLabel = i18n.translate( + 'xpack.drilldowns.components.DrilldownHelloBar.hideHelpButtonLabel', + { + defaultMessage: 'Hide', + } +); 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 deleted file mode 100644 index 3748fc666c81c..0000000000000 --- a/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.tsx +++ /dev/null @@ -1,21 +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'; - -// eslint-disable-next-line -export interface DrilldownPickerProps {} - -export const DrilldownPicker: React.FC = () => { - return ( - - ); -}; 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 deleted file mode 100644 index 4f024b7d9cd6a..0000000000000 --- a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.story.tsx +++ /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. - */ - -/* 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 deleted file mode 100644 index b45ac9197c7e0..0000000000000 --- a/x-pack/plugins/drilldowns/public/components/flyout_create_drilldown/flyout_create_drilldown.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * 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_drilldown_wizard/flyout_drilldown_wizard.story.tsx b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx new file mode 100644 index 0000000000000..152cd393b9d3e --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * 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 { FlyoutDrilldownWizard } from '.'; +import { + dashboardFactory, + urlFactory, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../advanced_ui_actions/public/components/action_wizard/test_data'; + +storiesOf('components/FlyoutDrilldownWizard', module) + .add('default', () => { + return ; + }) + .add('open in flyout - create', () => { + return ( + {}}> + {}} + drilldownActionFactories={[urlFactory, dashboardFactory]} + /> + + ); + }) + .add('open in flyout - edit', () => { + return ( + {}}> + {}} + drilldownActionFactories={[urlFactory, dashboardFactory]} + initialDrilldownWizardConfig={{ + name: 'My fancy drilldown', + actionFactory: urlFactory as any, + actionConfig: { + url: 'https://elastic.co', + openInNewTab: true, + }, + }} + mode={'edit'} + /> + + ); + }) + .add('open in flyout - edit, just 1 action type', () => { + return ( + {}}> + {}} + drilldownActionFactories={[dashboardFactory]} + initialDrilldownWizardConfig={{ + name: 'My fancy drilldown', + actionFactory: urlFactory as any, + actionConfig: { + url: 'https://elastic.co', + openInNewTab: true, + }, + }} + mode={'edit'} + /> + + ); + }); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx new file mode 100644 index 0000000000000..8541aae06ff0c --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx @@ -0,0 +1,140 @@ +/* + * 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, { useState } from 'react'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; +import { FormDrilldownWizard } from '../form_drilldown_wizard'; +import { FlyoutFrame } from '../flyout_frame'; +import { + txtCreateDrilldownButtonLabel, + txtCreateDrilldownTitle, + txtDeleteDrilldownButtonLabel, + txtEditDrilldownButtonLabel, + txtEditDrilldownTitle, +} from './i18n'; +import { DrilldownHelloBar } from '../drilldown_hello_bar'; +import { AdvancedUiActionsActionFactory as ActionFactory } from '../../../../advanced_ui_actions/public'; + +export interface DrilldownWizardConfig { + name: string; + actionFactory?: ActionFactory; + actionConfig?: ActionConfig; +} + +export interface FlyoutDrilldownWizardProps { + drilldownActionFactories: Array>; + + onSubmit?: (drilldownWizardConfig: Required) => void; + onDelete?: () => void; + onClose?: () => void; + onBack?: () => void; + + mode?: 'create' | 'edit'; + initialDrilldownWizardConfig?: DrilldownWizardConfig; + + showWelcomeMessage?: boolean; + onWelcomeHideClick?: () => void; + + actionFactoryContext?: object; +} + +export function FlyoutDrilldownWizard({ + onClose, + onBack, + onSubmit = () => {}, + initialDrilldownWizardConfig, + mode = 'create', + onDelete = () => {}, + showWelcomeMessage = true, + onWelcomeHideClick, + drilldownActionFactories, + actionFactoryContext, +}: FlyoutDrilldownWizardProps) { + const [wizardConfig, setWizardConfig] = useState( + () => + initialDrilldownWizardConfig ?? { + name: '', + } + ); + + const isActionValid = ( + config: DrilldownWizardConfig + ): config is Required => { + if (!wizardConfig.name) return false; + if (!wizardConfig.actionFactory) return false; + if (!wizardConfig.actionConfig) return false; + + return wizardConfig.actionFactory.isConfigValid(wizardConfig.actionConfig); + }; + + const footer = ( + { + if (isActionValid(wizardConfig)) { + onSubmit(wizardConfig); + } + }} + fill + isDisabled={!isActionValid(wizardConfig)} + data-test-subj={'drilldownWizardSubmit'} + > + {mode === 'edit' ? txtEditDrilldownButtonLabel : txtCreateDrilldownButtonLabel} + + ); + + return ( + } + > + { + setWizardConfig({ + ...wizardConfig, + name: newName, + }); + }} + actionConfig={wizardConfig.actionConfig} + onActionConfigChange={newActionConfig => { + setWizardConfig({ + ...wizardConfig, + actionConfig: newActionConfig, + }); + }} + currentActionFactory={wizardConfig.actionFactory} + onActionFactoryChange={actionFactory => { + if (!actionFactory) { + setWizardConfig({ + ...wizardConfig, + actionFactory: undefined, + actionConfig: undefined, + }); + } else { + setWizardConfig({ + ...wizardConfig, + actionFactory, + actionConfig: actionFactory.createConfig(), + }); + } + }} + actionFactories={drilldownActionFactories} + actionFactoryContext={actionFactoryContext!} + /> + {mode === 'edit' && ( + <> + + + {txtDeleteDrilldownButtonLabel} + + + )} + + ); +} diff --git a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/i18n.ts b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/i18n.ts new file mode 100644 index 0000000000000..a4a2754a444ab --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/i18n.ts @@ -0,0 +1,42 @@ +/* + * 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 txtCreateDrilldownTitle = i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.createDrilldownTitle', + { + defaultMessage: 'Create Drilldown', + } +); + +export const txtEditDrilldownTitle = i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.editDrilldownTitle', + { + defaultMessage: 'Edit Drilldown', + } +); + +export const txtCreateDrilldownButtonLabel = i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.createDrilldownButtonLabel', + { + defaultMessage: 'Create drilldown', + } +); + +export const txtEditDrilldownButtonLabel = i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.editDrilldownButtonLabel', + { + defaultMessage: 'Save', + } +); + +export const txtDeleteDrilldownButtonLabel = i18n.translate( + 'xpack.drilldowns.components.flyoutDrilldownWizard.deleteDrilldownButtonLabel', + { + defaultMessage: 'Delete drilldown', + } +); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/index.ts b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/index.ts new file mode 100644 index 0000000000000..96ed23bf112c9 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './flyout_drilldown_wizard'; 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 index 2715637f6392f..cb223db556f56 100644 --- 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 @@ -21,6 +21,13 @@ storiesOf('components/FlyoutFrame', module) .add('with onClose', () => { return console.log('onClose')}>test; }) + .add('with onBack', () => { + return ( + console.log('onClose')} title={'Title'}> + test + + ); + }) .add('custom footer', () => { return click me!}>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 index b5fb52fcf5c18..0a3989487745f 100644 --- 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 @@ -6,9 +6,11 @@ import React from 'react'; import { render } from 'react-dom'; -import { render as renderTestingLibrary, fireEvent } from '@testing-library/react'; +import { render as renderTestingLibrary, fireEvent, cleanup } from '@testing-library/react/pure'; import { FlyoutFrame } from '.'; +afterEach(cleanup); + describe('', () => { test('renders without crashing', () => { const div = document.createElement('div'); 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 index 2945cfd739482..b55cbd88d0dc0 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.tsx +++ b/x-pack/plugins/drilldowns/public/components/flyout_frame/flyout_frame.tsx @@ -13,13 +13,16 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, + EuiButtonIcon, } from '@elastic/eui'; -import { txtClose } from './i18n'; +import { txtClose, txtBack } from './i18n'; export interface FlyoutFrameProps { title?: React.ReactNode; footer?: React.ReactNode; + banner?: React.ReactNode; onClose?: () => void; + onBack?: () => void; } /** @@ -30,11 +33,31 @@ export const FlyoutFrame: React.FC = ({ footer, onClose, children, + onBack, + banner, }) => { - const headerFragment = title && ( + const headerFragment = (title || onBack) && ( -

    {title}

    + + {onBack && ( + +
    + +
    +
    + )} + {title && ( + +

    {title}

    +
    + )} +
    ); @@ -64,7 +87,7 @@ export const FlyoutFrame: React.FC = ({ return ( <> {headerFragment} - {children} + {children} {footerFragment} ); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_frame/i18n.ts b/x-pack/plugins/drilldowns/public/components/flyout_frame/i18n.ts index 257d7d36dbee1..23af89ebf9bc7 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_frame/i18n.ts +++ b/x-pack/plugins/drilldowns/public/components/flyout_frame/i18n.ts @@ -6,6 +6,10 @@ import { i18n } from '@kbn/i18n'; -export const txtClose = i18n.translate('xpack.drilldowns.components.FlyoutFrame.Close', { +export const txtClose = i18n.translate('xpack.drilldowns.components.FlyoutFrame.CloseButtonLabel', { defaultMessage: 'Close', }); + +export const txtBack = i18n.translate('xpack.drilldowns.components.FlyoutFrame.BackButtonLabel', { + defaultMessage: 'Back', +}); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.story.tsx b/x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.story.tsx new file mode 100644 index 0000000000000..0529f0451b16a --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.story.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { EuiFlyout } from '@elastic/eui'; +import { storiesOf } from '@storybook/react'; +import { FlyoutListManageDrilldowns } from './flyout_list_manage_drilldowns'; + +storiesOf('components/FlyoutListManageDrilldowns', module).add('default', () => ( + {}}> + + +)); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.tsx b/x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.tsx new file mode 100644 index 0000000000000..a44a7ccccb4dc --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FlyoutFrame } from '../flyout_frame'; +import { DrilldownListItem, ListManageDrilldowns } from '../list_manage_drilldowns'; +import { txtManageDrilldowns } from './i18n'; +import { DrilldownHelloBar } from '../drilldown_hello_bar'; + +export interface FlyoutListManageDrilldownsProps { + drilldowns: DrilldownListItem[]; + onClose?: () => void; + onCreate?: () => void; + onEdit?: (drilldownId: string) => void; + onDelete?: (drilldownIds: string[]) => void; + showWelcomeMessage?: boolean; + onWelcomeHideClick?: () => void; +} + +export function FlyoutListManageDrilldowns({ + drilldowns, + onClose = () => {}, + onCreate, + onDelete, + onEdit, + showWelcomeMessage = true, + onWelcomeHideClick, +}: FlyoutListManageDrilldownsProps) { + return ( + } + > + + + ); +} diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.story.tsx b/x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/i18n.ts similarity index 52% rename from x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.story.tsx rename to x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/i18n.ts index 5627a5d6f4522..0dd4e37d4dddd 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_picker/drilldown_picker.story.tsx +++ b/x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/i18n.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as React from 'react'; -import { storiesOf } from '@storybook/react'; -import { DrilldownPicker } from '.'; +import { i18n } from '@kbn/i18n'; -storiesOf('components/DrilldownPicker', module).add('default', () => { - return ; -}); +export const txtManageDrilldowns = i18n.translate( + 'xpack.drilldowns.components.FlyoutListManageDrilldowns.manageDrilldownsTitle', + { + defaultMessage: 'Manage Drilldowns', + } +); diff --git a/x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/index.ts b/x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/index.ts new file mode 100644 index 0000000000000..f8c9d224fb292 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/flyout_list_manage_drilldowns/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './flyout_list_manage_drilldowns'; 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 deleted file mode 100644 index e7e1d67473e8c..0000000000000 --- a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.story.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * 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.tsx b/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.tsx deleted file mode 100644 index 4422de604092b..0000000000000 --- a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.tsx +++ /dev/null @@ -1,52 +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 { 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_drilldown_wizard/form_drilldown_wizard.story.tsx b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.story.tsx new file mode 100644 index 0000000000000..2fc35eb6b5298 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.story.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { storiesOf } from '@storybook/react'; +import { FormDrilldownWizard } from '.'; + +const DemoEditName: React.FC = () => { + const [name, setName] = React.useState(''); + + return ( + <> + {' '} +
    name: {name}
    + + ); +}; + +storiesOf('components/FormDrilldownWizard', module) + .add('default', () => { + return ; + }) + .add('[name=foobar]', () => { + return ; + }) + .add('can edit name', () => ); 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_drilldown_wizard/form_drilldown_wizard.test.tsx similarity index 60% rename from x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.test.tsx rename to x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.test.tsx index 6691966e47e64..d9c53ae6f737a 100644 --- a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/form_create_drilldown.test.tsx +++ b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.test.tsx @@ -6,41 +6,39 @@ import React from 'react'; import { render } from 'react-dom'; -import { FormCreateDrilldown } from '.'; -import { render as renderTestingLibrary, fireEvent } from '@testing-library/react'; +import { FormDrilldownWizard } from './form_drilldown_wizard'; +import { render as renderTestingLibrary, fireEvent, cleanup } from '@testing-library/react/pure'; import { txtNameOfDrilldown } from './i18n'; -describe('', () => { +afterEach(cleanup); + +describe('', () => { test('renders without crashing', () => { const div = document.createElement('div'); - render( {}} />, div); + render( {}} actionFactoryContext={{}} />, div); }); describe('[name=]', () => { test('if name not provided, uses to empty string', () => { const div = document.createElement('div'); - render(, div); + render(, div); - const input = div.querySelector( - '[data-test-subj="dynamicActionNameInput"]' - ) as HTMLInputElement; + const input = div.querySelector('[data-test-subj="drilldownNameInput"]') as HTMLInputElement; expect(input?.value).toBe(''); }); - test('can set name input field value', () => { + test('can set initial name input field value', () => { const div = document.createElement('div'); - render(, div); + render(, div); - const input = div.querySelector( - '[data-test-subj="dynamicActionNameInput"]' - ) as HTMLInputElement; + const input = div.querySelector('[data-test-subj="drilldownNameInput"]') as HTMLInputElement; expect(input?.value).toBe('foo'); - render(, div); + render(, div); expect(input?.value).toBe('bar'); }); @@ -48,7 +46,7 @@ describe('', () => { test('fires onNameChange callback on name change', () => { const onNameChange = jest.fn(); const utils = renderTestingLibrary( - + ); const input = utils.getByLabelText(txtNameOfDrilldown); diff --git a/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx new file mode 100644 index 0000000000000..93b3710bf6cc6 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n'; +import { + AdvancedUiActionsActionFactory as ActionFactory, + ActionWizard, +} from '../../../../advanced_ui_actions/public'; + +const noopFn = () => {}; + +export interface FormDrilldownWizardProps { + name?: string; + onNameChange?: (name: string) => void; + + currentActionFactory?: ActionFactory; + onActionFactoryChange?: (actionFactory: ActionFactory | null) => void; + actionFactoryContext: object; + + actionConfig?: object; + onActionConfigChange?: (config: object) => void; + + actionFactories?: ActionFactory[]; +} + +export const FormDrilldownWizard: React.FC = ({ + name = '', + actionConfig, + currentActionFactory, + onNameChange = noopFn, + onActionConfigChange = noopFn, + onActionFactoryChange = noopFn, + actionFactories = [], + actionFactoryContext, +}) => { + const nameFragment = ( + + onNameChange(event.target.value)} + data-test-subj="drilldownNameInput" + /> + + ); + + const actionWizard = ( + 1 ? txtDrilldownAction : undefined} + fullWidth={true} + > + onActionFactoryChange(actionFactory)} + onConfigChange={config => onActionConfigChange(config)} + context={actionFactoryContext} + /> + + ); + + return ( + <> + + {nameFragment} + + {actionWizard} + + + ); +}; diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/i18n.ts similarity index 89% rename from x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts rename to x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/i18n.ts index 4c0e287935edd..e9b19ab0afa97 100644 --- a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/i18n.ts +++ b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/i18n.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const txtNameOfDrilldown = i18n.translate( 'xpack.drilldowns.components.FormCreateDrilldown.nameOfDrilldown', { - defaultMessage: 'Name of drilldown', + defaultMessage: 'Name', } ); @@ -23,6 +23,6 @@ export const txtUntitledDrilldown = i18n.translate( export const txtDrilldownAction = i18n.translate( 'xpack.drilldowns.components.FormCreateDrilldown.drilldownAction', { - defaultMessage: 'Drilldown action', + defaultMessage: 'Action', } ); diff --git a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/index.tsx similarity index 85% rename from x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx rename to x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/index.tsx index c2c5a7e435b39..4aea824de00d7 100644 --- a/x-pack/plugins/drilldowns/public/components/form_create_drilldown/index.tsx +++ b/x-pack/plugins/drilldowns/public/components/form_drilldown_wizard/index.tsx @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './form_create_drilldown'; +export * from './form_drilldown_wizard'; diff --git a/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/i18n.ts b/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/i18n.ts new file mode 100644 index 0000000000000..fbc7c9dcfb4a1 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/i18n.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * 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.ListManageDrilldowns.createDrilldownButtonLabel', + { + defaultMessage: 'Create new', + } +); + +export const txtEditDrilldown = i18n.translate( + 'xpack.drilldowns.components.ListManageDrilldowns.editDrilldownButtonLabel', + { + defaultMessage: 'Edit', + } +); + +export const txtDeleteDrilldowns = (count: number) => + i18n.translate('xpack.drilldowns.components.ListManageDrilldowns.deleteDrilldownsButtonLabel', { + defaultMessage: 'Delete ({count})', + values: { + count, + }, + }); + +export const txtSelectDrilldown = i18n.translate( + 'xpack.drilldowns.components.ListManageDrilldowns.selectThisDrilldownCheckboxLabel', + { + defaultMessage: 'Select this drilldown', + } +); diff --git a/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/index.tsx b/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/index.tsx new file mode 100644 index 0000000000000..82b6ce27af6d4 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/index.tsx @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './list_manage_drilldowns'; diff --git a/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.story.tsx b/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.story.tsx new file mode 100644 index 0000000000000..eafe50bab2016 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.story.tsx @@ -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 * as React from 'react'; +import { storiesOf } from '@storybook/react'; +import { ListManageDrilldowns } from './list_manage_drilldowns'; + +storiesOf('components/ListManageDrilldowns', module).add('default', () => ( + +)); diff --git a/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.test.tsx b/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.test.tsx new file mode 100644 index 0000000000000..4a4d67b08b1d3 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { cleanup, fireEvent, render } from '@testing-library/react/pure'; +import '@testing-library/jest-dom/extend-expect'; // TODO: this should be global +import { + DrilldownListItem, + ListManageDrilldowns, + TEST_SUBJ_DRILLDOWN_ITEM, +} from './list_manage_drilldowns'; + +// TODO: for some reason global cleanup from RTL doesn't work +// afterEach is not available for it globally during setup +afterEach(cleanup); + +const drilldowns: DrilldownListItem[] = [ + { id: '1', actionName: 'Dashboard', drilldownName: 'Drilldown 1' }, + { id: '2', actionName: 'Dashboard', drilldownName: 'Drilldown 2' }, + { id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3' }, +]; + +test('Render list of drilldowns', () => { + const screen = render(); + expect(screen.getAllByTestId(TEST_SUBJ_DRILLDOWN_ITEM)).toHaveLength(drilldowns.length); +}); + +test('Emit onEdit() when clicking on edit drilldown', () => { + const fn = jest.fn(); + const screen = render(); + + const editButtons = screen.getAllByText('Edit'); + expect(editButtons).toHaveLength(drilldowns.length); + fireEvent.click(editButtons[1]); + expect(fn).toBeCalledWith(drilldowns[1].id); +}); + +test('Emit onCreate() when clicking on create drilldown', () => { + const fn = jest.fn(); + const screen = render(); + fireEvent.click(screen.getByText('Create new')); + expect(fn).toBeCalled(); +}); + +test('Delete button is not visible when non is selected', () => { + const fn = jest.fn(); + const screen = render(); + expect(screen.queryByText(/Delete/i)).not.toBeInTheDocument(); + expect(screen.queryByText(/Create/i)).toBeInTheDocument(); +}); + +test('Can delete drilldowns', () => { + const fn = jest.fn(); + const screen = render(); + + const checkboxes = screen.getAllByLabelText(/Select this drilldown/i); + expect(checkboxes).toHaveLength(3); + + fireEvent.click(checkboxes[1]); + fireEvent.click(checkboxes[2]); + + expect(screen.queryByText(/Create/i)).not.toBeInTheDocument(); + + fireEvent.click(screen.getByText(/Delete \(2\)/i)); + + expect(fn).toBeCalledWith([drilldowns[1].id, drilldowns[2].id]); +}); diff --git a/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.tsx b/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.tsx new file mode 100644 index 0000000000000..ab51c0a829ed3 --- /dev/null +++ b/x-pack/plugins/drilldowns/public/components/list_manage_drilldowns/list_manage_drilldowns.tsx @@ -0,0 +1,122 @@ +/* + * 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 { + EuiBasicTable, + EuiBasicTableColumn, + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSpacer, + EuiTextColor, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import { + txtCreateDrilldown, + txtDeleteDrilldowns, + txtEditDrilldown, + txtSelectDrilldown, +} from './i18n'; + +export interface DrilldownListItem { + id: string; + actionName: string; + drilldownName: string; + icon?: string; +} + +export interface ListManageDrilldownsProps { + drilldowns: DrilldownListItem[]; + + onEdit?: (id: string) => void; + onCreate?: () => void; + onDelete?: (ids: string[]) => void; +} + +const noop = () => {}; + +export const TEST_SUBJ_DRILLDOWN_ITEM = 'listManageDrilldownsItem'; + +export function ListManageDrilldowns({ + drilldowns, + onEdit = noop, + onCreate = noop, + onDelete = noop, +}: ListManageDrilldownsProps) { + const [selectedDrilldowns, setSelectedDrilldowns] = useState([]); + + const columns: Array> = [ + { + field: 'drilldownName', + name: 'Name', + truncateText: true, + width: '50%', + 'data-test-subj': 'drilldownListItemName', + }, + { + name: 'Action', + render: (drilldown: DrilldownListItem) => ( + + {drilldown.icon && ( + + + + )} + + {drilldown.actionName} + + + ), + }, + { + align: 'right', + render: (drilldown: DrilldownListItem) => ( + onEdit(drilldown.id)}> + {txtEditDrilldown} + + ), + }, + ]; + + return ( + <> + { + setSelectedDrilldowns(selection.map(drilldown => drilldown.id)); + }, + selectableMessage: () => txtSelectDrilldown, + }} + rowProps={{ + 'data-test-subj': TEST_SUBJ_DRILLDOWN_ITEM, + }} + hasActions={true} + /> + + {selectedDrilldowns.length === 0 ? ( + onCreate()}> + {txtCreateDrilldown} + + ) : ( + onDelete(selectedDrilldowns)} + data-test-subj={'listManageDeleteDrilldowns'} + > + {txtDeleteDrilldowns(selectedDrilldowns.length)} + + )} + + ); +} diff --git a/x-pack/plugins/drilldowns/public/index.ts b/x-pack/plugins/drilldowns/public/index.ts index 63e7a12235462..f976356822dce 100644 --- a/x-pack/plugins/drilldowns/public/index.ts +++ b/x-pack/plugins/drilldowns/public/index.ts @@ -7,10 +7,10 @@ import { DrilldownsPlugin } from './plugin'; export { - DrilldownsSetupContract, - DrilldownsSetupDependencies, - DrilldownsStartContract, - DrilldownsStartDependencies, + SetupContract as DrilldownsSetup, + SetupDependencies as DrilldownsSetupDependencies, + StartContract as DrilldownsStart, + StartDependencies as DrilldownsStartDependencies, } from './plugin'; export function plugin() { diff --git a/x-pack/plugins/drilldowns/public/mocks.ts b/x-pack/plugins/drilldowns/public/mocks.ts index bfade1674072a..18816243a3572 100644 --- a/x-pack/plugins/drilldowns/public/mocks.ts +++ b/x-pack/plugins/drilldowns/public/mocks.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DrilldownsSetupContract, DrilldownsStartContract } from '.'; +import { DrilldownsSetup, DrilldownsStart } from '.'; -export type Setup = jest.Mocked; -export type Start = jest.Mocked; +export type Setup = jest.Mocked; +export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { @@ -17,12 +17,14 @@ const createSetupContract = (): Setup => { }; const createStartContract = (): Start => { - const startContract: Start = {}; + const startContract: Start = { + FlyoutManageDrilldowns: jest.fn(), + }; return startContract; }; -export const bfetchPluginMock = { +export const drilldownsPluginMock = { createSetupContract, createStartContract, }; diff --git a/x-pack/plugins/drilldowns/public/plugin.ts b/x-pack/plugins/drilldowns/public/plugin.ts index b89172541b91e..0108e04df9c99 100644 --- a/x-pack/plugins/drilldowns/public/plugin.ts +++ b/x-pack/plugins/drilldowns/public/plugin.ts @@ -6,52 +6,41 @@ import { CoreStart, CoreSetup, Plugin } from 'src/core/public'; import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; -import { DrilldownService } from './service'; -import { - FlyoutCreateDrilldownActionContext, - FlyoutEditDrilldownActionContext, - OPEN_FLYOUT_ADD_DRILLDOWN, - OPEN_FLYOUT_EDIT_DRILLDOWN, -} from './actions'; - -export interface DrilldownsSetupDependencies { +import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '../../advanced_ui_actions/public'; +import { createFlyoutManageDrilldowns } from './components/connected_flyout_manage_drilldowns'; +import { Storage } from '../../../../src/plugins/kibana_utils/public'; + +export interface SetupDependencies { uiActions: UiActionsSetup; + advancedUiActions: AdvancedUiActionsSetup; } -export interface DrilldownsStartDependencies { +export interface StartDependencies { uiActions: UiActionsStart; + advancedUiActions: AdvancedUiActionsStart; } -export type DrilldownsSetupContract = Pick; - // eslint-disable-next-line -export interface DrilldownsStartContract {} +export interface SetupContract {} -declare module '../../../../src/plugins/ui_actions/public' { - export interface ActionContextMapping { - [OPEN_FLYOUT_ADD_DRILLDOWN]: FlyoutCreateDrilldownActionContext; - [OPEN_FLYOUT_EDIT_DRILLDOWN]: FlyoutEditDrilldownActionContext; - } +export interface StartContract { + FlyoutManageDrilldowns: ReturnType; } export class DrilldownsPlugin - implements - Plugin< - DrilldownsSetupContract, - DrilldownsStartContract, - DrilldownsSetupDependencies, - DrilldownsStartDependencies - > { - private readonly service = new DrilldownService(); - - public setup(core: CoreSetup, plugins: DrilldownsSetupDependencies): DrilldownsSetupContract { - this.service.bootstrap(core, plugins); - - return this.service; + implements Plugin { + public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { + return {}; } - public start(core: CoreStart, plugins: DrilldownsStartDependencies): DrilldownsStartContract { - return {}; + public start(core: CoreStart, plugins: StartDependencies): StartContract { + return { + FlyoutManageDrilldowns: createFlyoutManageDrilldowns({ + advancedUiActions: plugins.advancedUiActions, + storage: new Storage(localStorage), + notifications: core.notifications, + }), + }; } public stop() {} diff --git a/x-pack/plugins/drilldowns/public/service/drilldown_service.ts b/x-pack/plugins/drilldowns/public/service/drilldown_service.ts deleted file mode 100644 index 7745c30b4e335..0000000000000 --- a/x-pack/plugins/drilldowns/public/service/drilldown_service.ts +++ /dev/null @@ -1,32 +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 { CoreSetup } from 'src/core/public'; -// import { CONTEXT_MENU_TRIGGER } from '../../../../../src/plugins/embeddable/public'; -import { FlyoutCreateDrilldownAction, FlyoutEditDrilldownAction } from '../actions'; -import { DrilldownsSetupDependencies } from '../plugin'; - -export class DrilldownService { - bootstrap(core: CoreSetup, { uiActions }: DrilldownsSetupDependencies) { - const overlays = async () => (await core.getStartServices())[0].overlays; - - const actionFlyoutCreateDrilldown = new FlyoutCreateDrilldownAction({ overlays }); - uiActions.registerAction(actionFlyoutCreateDrilldown); - // uiActions.attachAction(CONTEXT_MENU_TRIGGER, actionFlyoutCreateDrilldown); - - const actionFlyoutEditDrilldown = new FlyoutEditDrilldownAction({ overlays }); - uiActions.registerAction(actionFlyoutEditDrilldown); - // uiActions.attachAction(CONTEXT_MENU_TRIGGER, actionFlyoutEditDrilldown); - } - - /** - * Convenience method to register a drilldown. (It should set-up all the - * necessary triggers and actions.) - */ - registerDrilldown = (): void => { - throw new Error('not implemented'); - }; -} diff --git a/x-pack/plugins/embeddable_enhanced/README.md b/x-pack/plugins/embeddable_enhanced/README.md new file mode 100644 index 0000000000000..a0be90731fdb0 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/README.md @@ -0,0 +1 @@ +# X-Pack part of `embeddable` plugin diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json new file mode 100644 index 0000000000000..780a1d5d89870 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "embeddableEnhanced", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["embeddable", "advancedUiActions"] +} diff --git a/x-pack/plugins/embeddable_enhanced/public/actions/index.ts b/x-pack/plugins/embeddable_enhanced/public/actions/index.ts new file mode 100644 index 0000000000000..b47abd48fd269 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/actions/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './panel_notifications_action'; diff --git a/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.test.ts b/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.test.ts new file mode 100644 index 0000000000000..839379387e094 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.test.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 { PanelNotificationsAction } from './panel_notifications_action'; +import { EnhancedEmbeddableContext } from '../types'; +import { ViewMode } from '../../../../../src/plugins/embeddable/public'; + +const createContext = (events: unknown[] = [], isEditMode = false): EnhancedEmbeddableContext => + ({ + embeddable: { + getInput: () => + ({ + viewMode: isEditMode ? ViewMode.EDIT : ViewMode.VIEW, + } as unknown), + enhancements: { + dynamicActions: { + state: { + get: () => + ({ + events, + } as unknown), + }, + }, + }, + }, + } as EnhancedEmbeddableContext); + +describe('PanelNotificationsAction', () => { + describe('getDisplayName', () => { + test('returns "0" if embeddable has no events', async () => { + const context = createContext(); + const action = new PanelNotificationsAction(); + + const name = await action.getDisplayName(context); + expect(name).toBe('0'); + }); + + test('returns "2" if embeddable has two events', async () => { + const context = createContext([{}, {}]); + const action = new PanelNotificationsAction(); + + const name = await action.getDisplayName(context); + expect(name).toBe('2'); + }); + }); + + describe('isCompatible', () => { + test('returns false if not in "edit" mode', async () => { + const context = createContext([{}]); + const action = new PanelNotificationsAction(); + + const result = await action.isCompatible(context); + expect(result).toBe(false); + }); + + test('returns true when in "edit" mode', async () => { + const context = createContext([{}], true); + const action = new PanelNotificationsAction(); + + const result = await action.isCompatible(context); + expect(result).toBe(true); + }); + + test('returns false when no embeddable has no events', async () => { + const context = createContext([], true); + const action = new PanelNotificationsAction(); + + const result = await action.isCompatible(context); + expect(result).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.ts b/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.ts new file mode 100644 index 0000000000000..19e0ac2a5a6d8 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/actions/panel_notifications_action.ts @@ -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 { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public'; +import { ViewMode } from '../../../../../src/plugins/embeddable/public'; +import { EnhancedEmbeddableContext, EnhancedEmbeddable } from '../types'; + +export const ACTION_PANEL_NOTIFICATIONS = 'ACTION_PANEL_NOTIFICATIONS'; + +/** + * This action renders in "edit" mode number of events (dynamic action) a panel + * has attached to it. + */ +export class PanelNotificationsAction implements ActionDefinition { + public readonly id = ACTION_PANEL_NOTIFICATIONS; + + private getEventCount(embeddable: EnhancedEmbeddable): number { + return embeddable.enhancements.dynamicActions.state.get().events.length; + } + + public readonly getDisplayName = ({ embeddable }: EnhancedEmbeddableContext) => { + return String(this.getEventCount(embeddable)); + }; + + public readonly isCompatible = async ({ embeddable }: EnhancedEmbeddableContext) => { + if (embeddable.getInput().viewMode !== ViewMode.EDIT) return false; + return this.getEventCount(embeddable) > 0; + }; + + public readonly execute = async () => {}; +} diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts similarity index 72% rename from src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts rename to x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts index ddd84b0544345..f8b3a9dfb92d0 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_action_storage.test.ts +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.test.ts @@ -1,29 +1,18 @@ /* - * 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. + * 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 { Embeddable } from './embeddable'; -import { EmbeddableInput } from './i_embeddable'; -import { ViewMode } from '../types'; -import { EmbeddableActionStorage, SerializedEvent } from './embeddable_action_storage'; -import { of } from '../../../../kibana_utils/public'; +import { Embeddable, ViewMode } from '../../../../../src/plugins/embeddable/public'; +import { + EmbeddableActionStorage, + EmbeddableWithDynamicActionsInput, +} from './embeddable_action_storage'; +import { UiActionsEnhancedSerializedEvent } from '../../../advanced_ui_actions/public'; +import { of } from '../../../../../src/plugins/kibana_utils/public'; -class TestEmbeddable extends Embeddable { +class TestEmbeddable extends Embeddable { public readonly type = 'test'; constructor() { super({ id: 'test', viewMode: ViewMode.VIEW }, {}); @@ -42,62 +31,79 @@ describe('EmbeddableActionStorage', () => { test('can add event to embeddable', async () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event: SerializedEvent = { + const event: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: {} as any, }; - const events1 = embeddable.getInput().events || []; + const events1 = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events1).toEqual([]); await storage.create(event); - const events2 = embeddable.getInput().events || []; + const events2 = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events2).toEqual([event]); }); + test('does not merge .getInput() into .updateInput()', async () => { + const embeddable = new TestEmbeddable(); + const storage = new EmbeddableActionStorage(embeddable); + const event: UiActionsEnhancedSerializedEvent = { + eventId: 'EVENT_ID', + triggers: ['TRIGGER-ID'], + action: {} as any, + }; + + const spy = jest.spyOn(embeddable, 'updateInput'); + + await storage.create(event); + + expect(spy.mock.calls[0][0].id).toBe(undefined); + expect(spy.mock.calls[0][0].viewMode).toBe(undefined); + }); + test('can create multiple events', async () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event1: SerializedEvent = { + const event1: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID1', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: {} as any, }; - const event2: SerializedEvent = { + const event2: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID2', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: {} as any, }; - const event3: SerializedEvent = { + const event3: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID3', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: {} as any, }; - const events1 = embeddable.getInput().events || []; + const events1 = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events1).toEqual([]); await storage.create(event1); - const events2 = embeddable.getInput().events || []; + const events2 = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events2).toEqual([event1]); await storage.create(event2); await storage.create(event3); - const events3 = embeddable.getInput().events || []; + const events3 = embeddable.getInput().enhancements?.dynamicActions?.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 = { + const event: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: {} as any, }; @@ -122,16 +128,16 @@ describe('EmbeddableActionStorage', () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event1: SerializedEvent = { + const event1: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: { name: 'foo', } as any, }; - const event2: SerializedEvent = { + const event2: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: { name: 'bar', } as any, @@ -140,7 +146,7 @@ describe('EmbeddableActionStorage', () => { await storage.create(event1); await storage.update(event2); - const events = embeddable.getInput().events || []; + const events = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events).toEqual([event2]); }); @@ -148,30 +154,30 @@ describe('EmbeddableActionStorage', () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event1: SerializedEvent = { + const event1: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID1', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: { name: 'foo', } as any, }; - const event2: SerializedEvent = { + const event2: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID2', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: { name: 'bar', } as any, }; - const event22: SerializedEvent = { + const event22: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID2', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: { name: 'baz', } as any, }; - const event3: SerializedEvent = { + const event3: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID3', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: { name: 'qux', } as any, @@ -181,17 +187,17 @@ describe('EmbeddableActionStorage', () => { await storage.create(event2); await storage.create(event3); - const events1 = embeddable.getInput().events || []; + const events1 = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events1).toEqual([event1, event2, event3]); await storage.update(event22); - const events2 = embeddable.getInput().events || []; + const events2 = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events2).toEqual([event1, event22, event3]); await storage.update(event2); - const events3 = embeddable.getInput().events || []; + const events3 = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events3).toEqual([event1, event2, event3]); }); @@ -199,9 +205,9 @@ describe('EmbeddableActionStorage', () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event: SerializedEvent = { + const event: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: {} as any, }; @@ -217,14 +223,14 @@ describe('EmbeddableActionStorage', () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event1: SerializedEvent = { + const event1: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID1', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: {} as any, }; - const event2: SerializedEvent = { + const event2: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID2', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: {} as any, }; @@ -249,16 +255,16 @@ describe('EmbeddableActionStorage', () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event: SerializedEvent = { + const event: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: {} as any, }; await storage.create(event); await storage.remove(event.eventId); - const events = embeddable.getInput().events || []; + const events = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events).toEqual([]); }); @@ -266,23 +272,23 @@ describe('EmbeddableActionStorage', () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event1: SerializedEvent = { + const event1: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID1', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: { name: 'foo', } as any, }; - const event2: SerializedEvent = { + const event2: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID2', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: { name: 'bar', } as any, }; - const event3: SerializedEvent = { + const event3: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID3', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: { name: 'qux', } as any, @@ -292,22 +298,22 @@ describe('EmbeddableActionStorage', () => { await storage.create(event2); await storage.create(event3); - const events1 = embeddable.getInput().events || []; + const events1 = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events1).toEqual([event1, event2, event3]); await storage.remove(event2.eventId); - const events2 = embeddable.getInput().events || []; + const events2 = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events2).toEqual([event1, event3]); await storage.remove(event3.eventId); - const events3 = embeddable.getInput().events || []; + const events3 = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events3).toEqual([event1]); await storage.remove(event1.eventId); - const events4 = embeddable.getInput().events || []; + const events4 = embeddable.getInput().enhancements?.dynamicActions?.events || []; expect(events4).toEqual([]); }); @@ -327,9 +333,9 @@ describe('EmbeddableActionStorage', () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event: SerializedEvent = { + const event: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: {} as any, }; @@ -355,9 +361,9 @@ describe('EmbeddableActionStorage', () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event: SerializedEvent = { + const event: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: {} as any, }; @@ -383,9 +389,9 @@ describe('EmbeddableActionStorage', () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event: SerializedEvent = { + const event: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID', - triggerId: 'TRIGGER-ID', + triggers: ['TRIGGER-ID'], action: {} as any, }; @@ -402,19 +408,19 @@ describe('EmbeddableActionStorage', () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event1: SerializedEvent = { + const event1: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID1', - triggerId: 'TRIGGER-ID1', + triggers: ['TRIGGER-ID'], action: {} as any, }; - const event2: SerializedEvent = { + const event2: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID2', - triggerId: 'TRIGGER-ID2', + triggers: ['TRIGGER-ID'], action: {} as any, }; - const event3: SerializedEvent = { + const event3: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID3', - triggerId: 'TRIGGER-ID3', + triggers: ['TRIGGER-ID'], action: {} as any, }; @@ -458,7 +464,7 @@ describe('EmbeddableActionStorage', () => { await storage.create({ eventId: 'EVENT_ID1', - triggerId: 'TRIGGER-ID1', + triggers: ['TRIGGER-ID'], action: {} as any, }); @@ -466,7 +472,7 @@ describe('EmbeddableActionStorage', () => { await storage.create({ eventId: 'EVENT_ID2', - triggerId: 'TRIGGER-ID1', + triggers: ['TRIGGER-ID'], action: {} as any, }); @@ -502,15 +508,15 @@ describe('EmbeddableActionStorage', () => { const embeddable = new TestEmbeddable(); const storage = new EmbeddableActionStorage(embeddable); - const event1: SerializedEvent = { + const event1: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID1', - triggerId: 'TRIGGER-ID1', + triggers: ['TRIGGER-ID'], action: {} as any, }; - const event2: SerializedEvent = { + const event2: UiActionsEnhancedSerializedEvent = { eventId: 'EVENT_ID2', - triggerId: 'TRIGGER-ID1', + triggers: ['TRIGGER-ID'], action: {} as any, }; diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts new file mode 100644 index 0000000000000..dcb44323f6d11 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts @@ -0,0 +1,114 @@ +/* + * 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 { + UiActionsEnhancedAbstractActionStorage as AbstractActionStorage, + UiActionsEnhancedSerializedEvent as SerializedEvent, +} from '../../../advanced_ui_actions/public'; +import { + EmbeddableInput, + EmbeddableOutput, + IEmbeddable, +} from '../../../../../src/plugins/embeddable/public'; + +export interface EmbeddableWithDynamicActionsInput extends EmbeddableInput { + enhancements?: { + dynamicActions?: { + events: SerializedEvent[]; + }; + }; +} + +export type EmbeddableWithDynamicActions< + I extends EmbeddableWithDynamicActionsInput = EmbeddableWithDynamicActionsInput, + O extends EmbeddableOutput = EmbeddableOutput +> = IEmbeddable; + +export class EmbeddableActionStorage extends AbstractActionStorage { + constructor(private readonly embbeddable: EmbeddableWithDynamicActions) { + super(); + } + + private put(input: EmbeddableWithDynamicActionsInput, events: SerializedEvent[]) { + this.embbeddable.updateInput({ + enhancements: { + ...(input.enhancements || {}), + dynamicActions: { + ...(input.enhancements?.dynamicActions || {}), + events, + }, + }, + }); + } + + public async create(event: SerializedEvent) { + const input = this.embbeddable.getInput(); + const events = input.enhancements?.dynamicActions?.events || []; + 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.put(input, [...events, event]); + } + + public async update(event: SerializedEvent) { + const input = this.embbeddable.getInput(); + const events = input.enhancements?.dynamicActions?.events || []; + 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.put(input, [...events.slice(0, index), event, ...events.slice(index + 1)]); + } + + public async remove(eventId: string) { + const input = this.embbeddable.getInput(); + const events = input.enhancements?.dynamicActions?.events || []; + 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.put(input, [...events.slice(0, index), ...events.slice(index + 1)]); + } + + public async read(eventId: string): Promise { + const input = this.embbeddable.getInput(); + const events = input.enhancements?.dynamicActions?.events || []; + 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; + } + + public async list(): Promise { + const input = this.embbeddable.getInput(); + const events = input.enhancements?.dynamicActions?.events || []; + return events; + } +} diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/index.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/index.ts new file mode 100644 index 0000000000000..fabbc60a13f67 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/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 * from './is_enhanced_embeddable'; +export * from './embeddable_action_storage'; diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/is_enhanced_embeddable.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/is_enhanced_embeddable.ts new file mode 100644 index 0000000000000..f29430dc6a3de --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/is_enhanced_embeddable.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 { IEmbeddable } from '../../../../../src/plugins/embeddable/public'; +import { EnhancedEmbeddable } from '../types'; + +export const isEnhancedEmbeddable = ( + maybeEnhancedEmbeddable: E +): maybeEnhancedEmbeddable is EnhancedEmbeddable => + typeof (maybeEnhancedEmbeddable as EnhancedEmbeddable) + ?.enhancements?.dynamicActions === 'object'; diff --git a/x-pack/plugins/embeddable_enhanced/public/index.ts b/x-pack/plugins/embeddable_enhanced/public/index.ts new file mode 100644 index 0000000000000..059acf9644820 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'src/core/public'; +import { EmbeddableEnhancedPlugin } from './plugin'; + +export { + SetupContract as EmbeddableEnhancedSetupContract, + SetupDependencies as EmbeddableEnhancedSetupDependencies, + StartContract as EmbeddableEnhancedStartContract, + StartDependencies as EmbeddableEnhancedStartDependencies, +} from './plugin'; + +export function plugin(context: PluginInitializerContext) { + return new EmbeddableEnhancedPlugin(context); +} + +export { EnhancedEmbeddable, EnhancedEmbeddableContext } from './types'; +export { isEnhancedEmbeddable } from './embeddables'; diff --git a/x-pack/plugins/embeddable_enhanced/public/mocks.ts b/x-pack/plugins/embeddable_enhanced/public/mocks.ts new file mode 100644 index 0000000000000..d048d1248b6ff --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/mocks.ts @@ -0,0 +1,27 @@ +/* + * 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 { EmbeddableEnhancedSetupContract, EmbeddableEnhancedStartContract } from '.'; + +export type Setup = jest.Mocked; +export type Start = jest.Mocked; + +const createSetupContract = (): Setup => { + const setupContract: Setup = {}; + + return setupContract; +}; + +const createStartContract = (): Start => { + const startContract: Start = {}; + + return startContract; +}; + +export const embeddableEnhancedPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts new file mode 100644 index 0000000000000..d48c4f9e860cc --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -0,0 +1,160 @@ +/* + * 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 { CoreStart, CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; +import { SavedObjectAttributes } from 'kibana/public'; +import { + EmbeddableFactory, + EmbeddableFactoryDefinition, + EmbeddableInput, + EmbeddableOutput, + EmbeddableSetup, + EmbeddableStart, + IEmbeddable, + defaultEmbeddableFactoryProvider, + EmbeddableContext, + PANEL_NOTIFICATION_TRIGGER, +} from '../../../../src/plugins/embeddable/public'; +import { EnhancedEmbeddable, EnhancedEmbeddableContext } from './types'; +import { + EmbeddableActionStorage, + EmbeddableWithDynamicActions, +} from './embeddables/embeddable_action_storage'; +import { + UiActionsEnhancedDynamicActionManager as DynamicActionManager, + AdvancedUiActionsSetup, + AdvancedUiActionsStart, +} from '../../advanced_ui_actions/public'; +import { PanelNotificationsAction, ACTION_PANEL_NOTIFICATIONS } from './actions'; + +declare module '../../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [ACTION_PANEL_NOTIFICATIONS]: EnhancedEmbeddableContext; + } +} + +export interface SetupDependencies { + embeddable: EmbeddableSetup; + advancedUiActions: AdvancedUiActionsSetup; +} + +export interface StartDependencies { + embeddable: EmbeddableStart; + advancedUiActions: AdvancedUiActionsStart; +} + +// eslint-disable-next-line +export interface SetupContract {} + +// eslint-disable-next-line +export interface StartContract {} + +export class EmbeddableEnhancedPlugin + implements Plugin { + constructor(protected readonly context: PluginInitializerContext) {} + + private uiActions?: StartDependencies['advancedUiActions']; + + public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { + this.setCustomEmbeddableFactoryProvider(plugins); + + const panelNotificationAction = new PanelNotificationsAction(); + plugins.advancedUiActions.registerAction(panelNotificationAction); + plugins.advancedUiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id); + + return {}; + } + + public start(core: CoreStart, plugins: StartDependencies): StartContract { + this.uiActions = plugins.advancedUiActions; + + return {}; + } + + public stop() {} + + private setCustomEmbeddableFactoryProvider(plugins: SetupDependencies) { + plugins.embeddable.setCustomEmbeddableFactoryProvider( + < + I extends EmbeddableInput = EmbeddableInput, + O extends EmbeddableOutput = EmbeddableOutput, + E extends IEmbeddable = IEmbeddable, + T extends SavedObjectAttributes = SavedObjectAttributes + >( + def: EmbeddableFactoryDefinition + ): EmbeddableFactory => { + const factory: EmbeddableFactory = defaultEmbeddableFactoryProvider( + def + ); + return { + ...factory, + create: async (...args) => { + const embeddable = await factory.create(...args); + if (!embeddable) return embeddable; + return this.enhanceEmbeddableWithDynamicActions(embeddable); + }, + createFromSavedObject: async (...args) => { + const embeddable = await factory.createFromSavedObject(...args); + if (!embeddable) return embeddable; + return this.enhanceEmbeddableWithDynamicActions(embeddable); + }, + }; + } + ); + } + + private enhanceEmbeddableWithDynamicActions( + embeddable: E + ): EnhancedEmbeddable { + const enhancedEmbeddable = embeddable as EnhancedEmbeddable; + + const storage = new EmbeddableActionStorage(embeddable as EmbeddableWithDynamicActions); + const dynamicActions = new DynamicActionManager({ + isCompatible: async (context: unknown) => { + if (!(context as EmbeddableContext)?.embeddable) { + // eslint-disable-next-line no-console + console.warn('For drilldowns to work action context should contain .embeddable field.'); + return false; + } + + return (context as EmbeddableContext).embeddable.runtimeId === embeddable.runtimeId; + }, + storage, + uiActions: this.uiActions!, + }); + + dynamicActions.start().catch(error => { + /* eslint-disable */ + console.log('Failed to start embeddable dynamic actions', embeddable); + console.error(error); + /* eslint-enable */ + }); + + const stop = () => { + dynamicActions.stop().catch(error => { + /* eslint-disable */ + console.log('Failed to stop embeddable dynamic actions', embeddable); + console.error(error); + /* eslint-enable */ + }); + }; + + embeddable.getInput$().subscribe({ + next: () => { + storage.reload$.next(); + }, + error: stop, + complete: stop, + }); + + enhancedEmbeddable.enhancements = { + ...enhancedEmbeddable.enhancements, + dynamicActions, + }; + + return enhancedEmbeddable; + } +} diff --git a/x-pack/plugins/embeddable_enhanced/public/types.ts b/x-pack/plugins/embeddable_enhanced/public/types.ts new file mode 100644 index 0000000000000..924605be332b2 --- /dev/null +++ b/x-pack/plugins/embeddable_enhanced/public/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 { IEmbeddable } from '../../../../src/plugins/embeddable/public'; +import { UiActionsEnhancedDynamicActionManager as DynamicActionManager } from '../../advanced_ui_actions/public'; + +export type EnhancedEmbeddable = E & { + enhancements: { + /** + * Default implementation of dynamic action manager for embeddables. + */ + dynamicActions: DynamicActionManager; + }; +}; + +export interface EnhancedEmbeddableContext { + embeddable: EnhancedEmbeddable; +} diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index c40e7ad373eaf..66366cc0b520d 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -143,8 +143,7 @@ export class ReportingPublicPlugin implements Plugin { }, }); - uiActions.registerAction(action); - uiActions.attachAction(CONTEXT_MENU_TRIGGER, action); + uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, action); share.register(csvReportingProvider({ apiClient, toasts, license$ })); share.register( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d1270ea92c51e..481dfffd2e3a0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -886,7 +886,6 @@ "embeddableApi.addPanel.noMatchingObjectsMessage": "一致するオブジェクトが見つかりませんでした。", "embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} が追加されました", "embeddableApi.addPanel.Title": "パネルの追加", - "embeddableApi.customizePanel.action.displayName": "パネルをカスタマイズ", "embeddableApi.customizePanel.modal.cancel": "キャンセル", "embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleFormRowLabel": "パネルタイトル", "embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel": "パネルのカスタムタイトルを入力してください", @@ -6250,13 +6249,9 @@ "xpack.data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "1つ以上の引数", "xpack.data.query.queryBar.cancelLongQuery": "キャンセル", "xpack.data.query.queryBar.runBeyond": "タイムアウトを越えて実行", - "xpack.drilldowns.components.FlyoutCreateDrilldown.CreateDrilldown": "ドリルダウンを作成", - "xpack.drilldowns.components.FlyoutFrame.Close": "閉じる", "xpack.drilldowns.components.FormCreateDrilldown.drilldownAction": "ドリルダウンアクション", "xpack.drilldowns.components.FormCreateDrilldown.nameOfDrilldown": "ドリルダウンの名前", "xpack.drilldowns.components.FormCreateDrilldown.untitledDrilldown": "無題のドリルダウン", - "xpack.drilldowns.FlyoutCreateDrilldownAction.displayName": "ドリルダウンを作成", - "xpack.drilldowns.panel.openFlyoutEditDrilldown.displayName": "ドリルダウンを管理", "xpack.endpoint.alertList.viewTitle": "アラートは有効な Rison エンコード文字列でなければなりません", "xpack.endpoint.alerts.errors.bad_rison": "", "xpack.endpoint.alerts.errors.before_cannot_be_used_with_after": "[before] を [after] と併用することはできません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 32c91a6ef2931..ca0e070c9bfd4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -887,7 +887,6 @@ "embeddableApi.addPanel.noMatchingObjectsMessage": "未找到任何匹配对象。", "embeddableApi.addPanel.savedObjectAddedToContainerSuccessMessageTitle": "{savedObjectName} 已添加", "embeddableApi.addPanel.Title": "添加面板", - "embeddableApi.customizePanel.action.displayName": "定制面板", "embeddableApi.customizePanel.modal.cancel": "取消", "embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleFormRowLabel": "面板标题", "embeddableApi.customizePanel.modal.optionsMenuForm.panelTitleInputAriaLabel": "为面板输入定制标题", @@ -6255,13 +6254,9 @@ "xpack.data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "一个或多个参数", "xpack.data.query.queryBar.cancelLongQuery": "取消", "xpack.data.query.queryBar.runBeyond": "运行超时", - "xpack.drilldowns.components.FlyoutCreateDrilldown.CreateDrilldown": "创建向下钻取", - "xpack.drilldowns.components.FlyoutFrame.Close": "关闭", "xpack.drilldowns.components.FormCreateDrilldown.drilldownAction": "向下钻取操作", "xpack.drilldowns.components.FormCreateDrilldown.nameOfDrilldown": "向下钻取的名称", "xpack.drilldowns.components.FormCreateDrilldown.untitledDrilldown": "未命名向下钻取", - "xpack.drilldowns.FlyoutCreateDrilldownAction.displayName": "创建向下钻取", - "xpack.drilldowns.panel.openFlyoutEditDrilldown.displayName": "管理向下钻取", "xpack.endpoint.alertList.viewTitle": "告警", "xpack.endpoint.alerts.errors.bad_rison": "必须是有效的 rison 编码字符串", "xpack.endpoint.alerts.errors.before_cannot_be_used_with_after": "[before] 不能与 [after] 一起使用", diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts new file mode 100644 index 0000000000000..1a90d5d1fe52a --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts @@ -0,0 +1,176 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const DASHBOARD_WITH_PIE_CHART_NAME = 'Dashboard with Pie Chart'; +const DASHBOARD_WITH_AREA_CHART_NAME = 'Dashboard With Area Chart'; + +const DRILLDOWN_TO_PIE_CHART_NAME = 'Go to pie chart dashboard'; +const DRILLDOWN_TO_AREA_CHART_NAME = 'Go to area chart dashboard'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); + const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); + const PageObjects = getPageObjects(['dashboard', 'common', 'header', 'timePicker']); + const kibanaServer = getService('kibanaServer'); + const esArchiver = getService('esArchiver'); + const pieChart = getService('pieChart'); + const log = getService('log'); + const browser = getService('browser'); + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + const filterBar = getService('filterBar'); + + describe('Dashboard Drilldowns', function() { + before(async () => { + log.debug('Dashboard Drilldowns:initTests'); + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.load('dashboard/drilldowns'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + }); + + after(async () => { + await esArchiver.unload('dashboard/drilldowns'); + }); + + it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { + await PageObjects.dashboard.gotoDashboardEditMode(DASHBOARD_WITH_PIE_CHART_NAME); + + // create drilldown + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); + await dashboardDrilldownPanelActions.clickCreateDrilldown(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); + await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ + drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, + destinationDashboardTitle: DASHBOARD_WITH_AREA_CHART_NAME, + }); + await dashboardDrilldownsManage.saveChanges(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); + + // save dashboard, navigate to view mode + await PageObjects.dashboard.saveDashboard(DASHBOARD_WITH_PIE_CHART_NAME, { + saveAsNew: false, + waitDialogIsClosed: true, + }); + + // trigger drilldown action by clicking on a pie and picking drilldown action by it's name + await pieChart.filterOnPieSlice('40,000'); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + const href = await dashboardDrilldownPanelActions.getActionHrefByText( + DRILLDOWN_TO_AREA_CHART_NAME + ); + expect(typeof href).to.be('string'); // checking that action has a href + const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); + }); + // checking that href is at least pointing to the same dashboard that we are navigated to by regular click + expect(dashboardIdFromHref).to.be(await PageObjects.dashboard.getDashboardIdFromCurrentUrl()); + + // check that we drilled-down with filter from pie chart + expect(await filterBar.getFilterCount()).to.be(1); + + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + + // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) + expect(await filterBar.getFilterCount()).to.be(1); + await pieChart.expectPieSliceCount(1); + + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + // delete drilldown + await PageObjects.dashboard.switchToEditMode(); + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); + await dashboardDrilldownPanelActions.clickManageDrilldowns(); + await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); + + await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); + await dashboardDrilldownsManage.closeFlyout(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + }); + + it('browser back/forward navigation works after drilldown navigation', async () => { + await PageObjects.dashboard.loadSavedDashboard(DASHBOARD_WITH_AREA_CHART_NAME); + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + await navigateWithinDashboard(async () => { + await browser.goBack(); + }); + + expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( + originalTimeRangeDurationHours + ); + }); + }); + + // utils which shouldn't be a part of test flow, but also too specific to be moved to pageobject or service + async function brushAreaChart() { + const areaChart = await testSubjects.find('visualizationLoader'); + expect(await areaChart.getAttribute('data-title')).to.be('Visualization漢字 AreaChart'); + await browser.dragAndDrop( + { + location: areaChart, + offset: { + x: -100, + y: 0, + }, + }, + { + location: areaChart, + offset: { + x: 100, + y: 0, + }, + } + ); + } + + async function navigateWithinDashboard(navigationTrigger: () => Promise) { + // before executing action which would trigger navigation: remember current dashboard id in url + const oldDashboardId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + // execute navigation action + await navigationTrigger(); + // wait until dashboard navigates to a new dashboard with area chart + await retry.waitFor('navigate to different dashboard', async () => { + const newDashboardId = await PageObjects.dashboard.getDashboardIdFromCurrentUrl(); + return typeof newDashboardId === 'string' && oldDashboardId !== newDashboardId; + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + } +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts new file mode 100644 index 0000000000000..ab273018dc3f7 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('drilldowns', function() { + this.tags(['skipFirefox']); + loadTestFile(require.resolve('./dashboard_drilldowns')); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 23825836caad3..2c8ac93c53fef 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -12,5 +12,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); loadTestFile(require.resolve('./reporting')); + loadTestFile(require.resolve('./drilldowns')); }); } diff --git a/x-pack/test/functional/es_archives/dashboard/drilldowns/data.json.gz b/x-pack/test/functional/es_archives/dashboard/drilldowns/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..a9b23ca7a579bdd2de633c67e386a31313162e2c GIT binary patch literal 2662 zcmV-s3YqmEiwFoonWQOZ*BnXT-|cpxD~$7Q?R=1L^3JKlBKISwl{U_ z*d9rn%*OU`AQBYOkOY?iEh`>9M|<7-zCfo>)0y@;dH{kHDanqr>jV7i;;teF7{9waCx6-id=jrT3esV z6h$bfGnyvOstZFsULpX=#mq@a%n6M|7ZaZ_1O9Oz8)_IsKJ`1*t9&Rzp=9=0F``)tU;pu+fBx;?fB#GK7;!W~(*S?F zK5{N9;}n9fksnjrIuG)mZ1gd@#qP!Q&)DJbF-Mhd1XCC#jz5;H{c(J8E_%Da&Sbc5 z46hpjoiUD>-~9b`~94$-ybHW3up6!Y^whEn+F-LqQ%Cg*eixZY<%qmj}lg}v8<*-tS zb38XHV@sig(PT3hV@pzu5)`sb2Qf}A(M;T7nb5?1I=@5-Cny-vxwywe0mG^VEJ7T1 z$Pa_y4Bz6QXcrnnvzfTQ66SE_h*p&Who%viB#9x zxO2UyY3h8&d_zEw;2`U0y5N|}b`pUtA%uY3HccTnzT+5t$hD`yq^p=%-atzLF8bsvxjj3fIFshiQj6c9pg#s>+p4CfowJ6yj<$!Dq691~Q^ z`{_hP53isF-bP(0rtgXzIP9o6-6t{#6MVl>4kw?fSsbHnO zuPASWRH)a=ug_@`3ZbwlpH{=na{+K9${M!>GHH^l*(ZaoR;OjkTLQGm zV)wd{5j?>@mjn_*owXMWuM?(8Pz2?EBC)4XC;4wom zz6fwO&~;O)}JvxKiH0lC_;yIF^i;(>DLoUt2-}F4L8*!adpJ?9RcJTIrPqbIhO#V|pjmNqIbbCk zGuSHMBl3=vJFgU^T?-DtACOikFMVAt0vXzq8vHBQx|5ycNpQ= z7VX+!jedqEuAPgA-aRxLP0n*@R*YQQ*O`qUNCd)tYcjW>Q8{)tp;zUgOnfp6Ipu3= zc=~jh_K23aqO*<^U1-*VT(M)iCz&+YlJ+x|0?!vCWfZF)w$@{AauZhcmQdT4fXb^& zc1(#{me_f{0-DhbG;&r+aaA9u$vs*wIBQQx8>=(5;EKgGV!GLQ6Jo~n7Y+FY7_Cvo zyEds_AI367)@-pmrqfh%s#1FdZ^aeOnUW=r<|Q-zzF8FCwLvxAC@&_rH7~E!FIs_* z8Z2-_z-pgns-11h%YwRBv+Fc}yyZ=?cD5@fxa+4j-3dCcw(F;vI?NodeK@aU?P|5< zN7w3W_ExC-GZp!1nn#jt-=@{)lh4{gUu|JIf0vEZX+W5-dx_Xy|I}m0v%>cCe4qPk zIA9J<@UKUjZ)4%6QQ3GB;vdAP=I3g=@d|$%+xDlFzIK0bLvr=2_V2O@>5^dG`yAI; zc*)z4wu!pKeMm9MV&s{&GZJ=8qm-{gQvWXGuWx}VE#sB;5_ZixZA4aQ1)PycdvLDv z@PeaL9Tc!t;vEL5*c$wtQ7TRj$Tk(0o@F7W+!vyjHwepsh-n~8mtkd>^46`(E>vZ* zkGGi5F^(?9(qUR+0HG`%?wgEqJKpq31&8bvIzKH(BeoLCTQVrKK z24$-xytWG+X*8t_=FJ`qHszfjE3#5*YO7XX0tbN2TjYusEMqu+r*u& z)oTw>dw_f2jz&fQk3vm3qRm zGU5F*5|x6~CoaB35e_CI{#WxpWnp8{UUYP|b~Hu5)mKMTi1QlM85(epsNvdOF~qex zY(v1RcmZ9g|4CLa_Bwrc-!q%2k9*B-ue0Agu&~?g47~Q#LdfZP-Arc5$7zAjPkT^! zYBzdx`ZjUd?x zc*{|mh|@#Brg#K?h*xg2YWzVu;%d?sGy@z%p`yk+J{Dk%w z#m94Tw#ZF(yoj{#2z%$n?lwWN!MUo_kO}q@%={dvM2rb11)kuiWN?Ke=2~J6@ec_N zg&(&~YrkoN3I-FaKkRgemf30#`k&S(JfZXhaf+_1jT2h#s@<8G=Fl7rt$xcoFxR?L zIsG^{i1CqdU#4?oBcGR2_QuR { + log.debug(`getActionWebElement: "${text}"`); + const menu = await testSubjects.find('multipleActionsContextMenu'); + const items = await menu.findAllByCssSelector('[data-test-subj*="embeddablePanelAction-"]'); + for (const item of items) { + const currentText = await item.getVisibleText(); + if (currentText === text) { + return item; + } + } + + throw new Error(`No action matching text "${text}"`); + } + })(); +} diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index aec91ba9e9034..f1d84f3054aa0 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -49,6 +49,10 @@ import { InfraSourceConfigurationFormProvider } from './infra_source_configurati import { LogsUiProvider } from './logs_ui'; import { MachineLearningProvider } from './ml'; import { TransformProvider } from './transform'; +import { + DashboardDrilldownPanelActionsProvider, + DashboardDrilldownsManageProvider, +} from './dashboard'; // define the name and providers for services that should be // available to your tests. If you don't specify anything here @@ -91,4 +95,6 @@ export const services = { logsUi: LogsUiProvider, ml: MachineLearningProvider, transform: TransformProvider, + dashboardDrilldownPanelActions: DashboardDrilldownPanelActionsProvider, + dashboardDrilldownsManage: DashboardDrilldownsManageProvider, }; From a532a91d7515a1075d3df32dd9c1b20081e2a641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 4 May 2020 16:15:43 +0200 Subject: [PATCH 083/122] [APM] Fix duplicate index patterns (#64883) --- x-pack/plugins/apm/server/tutorial/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index d8cbff9a1c27d..76e2456afa5df 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -13,6 +13,7 @@ import { ArtifactsSchema, TutorialsCategory } from '../../../../../src/plugins/home/server'; +import { APM_STATIC_INDEX_PATTERN_ID } from '../../common/index_pattern_constants'; const apmIntro = i18n.translate('xpack.apm.tutorial.introduction', { defaultMessage: @@ -39,6 +40,7 @@ export const tutorialProvider = ({ const savedObjects = [ { ...apmIndexPattern, + id: APM_STATIC_INDEX_PATTERN_ID, attributes: { ...apmIndexPattern.attributes, title: indexPatternTitle From f8349f6ce0c6b8d932088455e79dc747766fa850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 4 May 2020 15:22:18 +0100 Subject: [PATCH 084/122] [APM] Agent remote config: validation for Java agent configs (#63956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * validating java settings * adding min max support to duration * Agent config cleanup * refactoring * refactoring * refactoring * fixing i18n * validating min and max bytes * refactoring * refactoring * refactoring * accept number and string on amountAndUnitToString Co-authored-by: Elastic Machine Co-authored-by: Søren Louv-Jansen --- .../agent_configuration/amount_and_unit.ts | 11 +- .../agent_configuration_intake_rt.ts | 4 +- .../runtime_types/bytes_rt.test.ts | 97 +++++++++--- .../runtime_types/bytes_rt.ts | 65 +++++--- .../runtime_types/duration_rt.test.ts | 148 ++++++++++++------ .../runtime_types/duration_rt.ts | 43 +++-- .../runtime_types/float_rt.test.ts | 36 +++++ .../runtime_types/float_rt.ts | 28 ++++ .../runtime_types/get_range_type_message.ts | 41 +++++ .../runtime_types/integer_rt.test.ts | 70 ++++++--- .../runtime_types/integer_rt.ts | 19 ++- .../runtime_types/number_float_rt.test.ts | 36 ----- .../runtime_types/number_float_rt.ts | 36 ----- .../__snapshots__/index.test.ts.snap | 35 ++--- .../setting_definitions/general_settings.ts | 19 +-- .../setting_definitions/index.ts | 110 +++++++------ .../setting_definitions/java_settings.ts | 7 +- .../setting_definitions/types.d.ts | 47 +++--- .../SettingsPage/SettingFormRow.tsx | 19 ++- .../SettingsPage/SettingsPage.tsx | 4 +- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 22 files changed, 539 insertions(+), 344 deletions(-) create mode 100644 x-pack/plugins/apm/common/agent_configuration/runtime_types/float_rt.test.ts create mode 100644 x-pack/plugins/apm/common/agent_configuration/runtime_types/float_rt.ts create mode 100644 x-pack/plugins/apm/common/agent_configuration/runtime_types/get_range_type_message.ts delete mode 100644 x-pack/plugins/apm/common/agent_configuration/runtime_types/number_float_rt.test.ts delete mode 100644 x-pack/plugins/apm/common/agent_configuration/runtime_types/number_float_rt.ts diff --git a/x-pack/plugins/apm/common/agent_configuration/amount_and_unit.ts b/x-pack/plugins/apm/common/agent_configuration/amount_and_unit.ts index d6520ae150539..cd64b3025a65b 100644 --- a/x-pack/plugins/apm/common/agent_configuration/amount_and_unit.ts +++ b/x-pack/plugins/apm/common/agent_configuration/amount_and_unit.ts @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -interface AmountAndUnit { - amount: string; +export interface AmountAndUnit { + amount: number; unit: string; } export function amountAndUnitToObject(value: string): AmountAndUnit { // matches any postive and negative number and its unit. const [, amount = '', unit = ''] = value.match(/(^-?\d+)?(\w+)?/) || []; - return { amount, unit }; + return { amount: parseInt(amount, 10), unit }; } -export function amountAndUnitToString({ amount, unit }: AmountAndUnit) { +export function amountAndUnitToString({ + amount, + unit +}: Omit & { amount: string | number }) { return `${amount}${unit}`; } diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/agent_configuration_intake_rt.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/agent_configuration_intake_rt.ts index a0b1d5015b9ef..e903a56486b6e 100644 --- a/x-pack/plugins/apm/common/agent_configuration/runtime_types/agent_configuration_intake_rt.ts +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/agent_configuration_intake_rt.ts @@ -6,11 +6,11 @@ import * as t from 'io-ts'; import { settingDefinitions } from '../setting_definitions'; +import { SettingValidation } from '../setting_definitions/types'; // retrieve validation from config definitions settings and validate on the server const knownSettings = settingDefinitions.reduce< - // TODO: is it possible to get rid of any? - Record> + Record >((acc, { key, validation }) => { acc[key] = validation; return acc; diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/bytes_rt.test.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/bytes_rt.test.ts index 596037645c002..4d786605b00c7 100644 --- a/x-pack/plugins/apm/common/agent_configuration/runtime_types/bytes_rt.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/bytes_rt.test.ts @@ -4,35 +4,88 @@ * you may not use this file except in compliance with the Elastic License. */ -import { bytesRt } from './bytes_rt'; +import { getBytesRt } from './bytes_rt'; import { isRight } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; describe('bytesRt', () => { - describe('it should not accept', () => { - [ - undefined, - null, - '', - 0, - 'foo', - true, - false, - '100', - 'mb', - '0kb', - '5gb', - '6tb' - ].map(input => { - it(`${JSON.stringify(input)}`, () => { - expect(isRight(bytesRt.decode(input))).toBe(false); + describe('must accept any amount and unit', () => { + const bytesRt = getBytesRt({}); + describe('it should not accept', () => { + ['mb', 1, '1', '5gb', '6tb'].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(bytesRt.decode(input))).toBe(false); + }); + }); + }); + + describe('it should accept', () => { + ['-1b', '0mb', '1b', '2kb', '3mb', '1000mb'].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(bytesRt.decode(input))).toBe(true); + }); }); }); }); + describe('must be at least 0b', () => { + const bytesRt = getBytesRt({ + min: '0b' + }); + + describe('it should not accept', () => { + ['mb', '-1kb', '5gb', '6tb'].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(bytesRt.decode(input))).toBe(false); + }); + }); + }); + + describe('it should return correct error message', () => { + ['-1kb', '5gb', '6tb'].map(input => { + it(`${JSON.stringify(input)}`, () => { + const result = bytesRt.decode(input); + const message = PathReporter.report(result)[0]; + expect(message).toEqual('Must be greater than 0b'); + expect(isRight(result)).toBeFalsy(); + }); + }); + }); - describe('it should accept', () => { - ['1b', '2kb', '3mb'].map(input => { - it(`${JSON.stringify(input)}`, () => { - expect(isRight(bytesRt.decode(input))).toBe(true); + describe('it should accept', () => { + ['1b', '2kb', '3mb'].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(bytesRt.decode(input))).toBe(true); + }); + }); + }); + }); + describe('must be between 500b and 1kb', () => { + const bytesRt = getBytesRt({ + min: '500b', + max: '1kb' + }); + describe('it should not accept', () => { + ['mb', '-1b', '1b', '499b', '1025b', '2kb', '1mb'].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(bytesRt.decode(input))).toBe(false); + }); + }); + }); + describe('it should return correct error message', () => { + ['-1b', '1b', '499b', '1025b', '2kb', '1mb'].map(input => { + it(`${JSON.stringify(input)}`, () => { + const result = bytesRt.decode(input); + const message = PathReporter.report(result)[0]; + expect(message).toEqual('Must be between 500b and 1kb'); + expect(isRight(result)).toBeFalsy(); + }); + }); + }); + describe('it should accept', () => { + ['500b', '1024b', '1kb'].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(bytesRt.decode(input))).toBe(true); + }); }); }); }); diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/bytes_rt.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/bytes_rt.ts index d189fab89ae5d..9f49527438b49 100644 --- a/x-pack/plugins/apm/common/agent_configuration/runtime_types/bytes_rt.ts +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/bytes_rt.ts @@ -7,27 +7,50 @@ import * as t from 'io-ts'; import { either } from 'fp-ts/lib/Either'; import { amountAndUnitToObject } from '../amount_and_unit'; +import { getRangeTypeMessage } from './get_range_type_message'; -export const BYTE_UNITS = ['b', 'kb', 'mb']; +function toBytes(amount: number, unit: string) { + switch (unit) { + case 'b': + return amount; + case 'kb': + return amount * 2 ** 10; + case 'mb': + return amount * 2 ** 20; + } +} -export const bytesRt = new t.Type( - 'bytesRt', - t.string.is, - (input, context) => { - return either.chain(t.string.validate(input, context), inputAsString => { - const { amount, unit } = amountAndUnitToObject(inputAsString); - const amountAsInt = parseInt(amount, 10); - const isValidUnit = BYTE_UNITS.includes(unit); - const isValid = amountAsInt > 0 && isValidUnit; +function amountAndUnitToBytes(value?: string): number | undefined { + if (value) { + const { amount, unit } = amountAndUnitToObject(value); + if (isFinite(amount) && unit) { + return toBytes(amount, unit); + } + } +} - return isValid - ? t.success(inputAsString) - : t.failure( - input, - context, - `Must have numeric amount and a valid unit (${BYTE_UNITS})` - ); - }); - }, - t.identity -); +export function getBytesRt({ min, max }: { min?: string; max?: string }) { + const minAsBytes = amountAndUnitToBytes(min) ?? -Infinity; + const maxAsBytes = amountAndUnitToBytes(max) ?? Infinity; + const message = getRangeTypeMessage(min, max); + + return new t.Type( + 'bytesRt', + t.string.is, + (input, context) => { + return either.chain(t.string.validate(input, context), inputAsString => { + const inputAsBytes = amountAndUnitToBytes(inputAsString); + + const isValidAmount = + inputAsBytes !== undefined && + inputAsBytes >= minAsBytes && + inputAsBytes <= maxAsBytes; + + return isValidAmount + ? t.success(inputAsString) + : t.failure(input, context, message); + }); + }, + t.identity + ); +} diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.test.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.test.ts index 98d0cb5f028c3..ebfd9d9a72704 100644 --- a/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.test.ts @@ -4,62 +4,122 @@ * 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 { durationRt, getDurationRt } from './duration_rt'; +import { getDurationRt } from './duration_rt'; import { isRight } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; -describe('durationRt', () => { - describe('it should not accept', () => { - [ - undefined, - null, - '', - 0, - 'foo', - true, - false, - '100', - 's', - 'm', - '0ms', - '-1ms' - ].map(input => { - it(`${JSON.stringify(input)}`, () => { - expect(isRight(durationRt.decode(input))).toBe(false); +describe('getDurationRt', () => { + describe('must be at least 1m', () => { + const customDurationRt = getDurationRt({ min: '1m' }); + describe('it should not accept', () => { + [ + undefined, + null, + '', + 0, + 'foo', + true, + false, + '0m', + '-1m', + '1ms', + '1s' + ].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(customDurationRt.decode(input))).toBeFalsy(); + }); }); }); - }); - - describe('it should accept', () => { - ['1000ms', '2s', '3m', '1s'].map(input => { - it(`${JSON.stringify(input)}`, () => { - expect(isRight(durationRt.decode(input))).toBe(true); + describe('it should return correct error message', () => { + ['0m', '-1m', '1ms', '1s'].map(input => { + it(`${JSON.stringify(input)}`, () => { + const result = customDurationRt.decode(input); + const message = PathReporter.report(result)[0]; + expect(message).toEqual('Must be greater than 1m'); + expect(isRight(result)).toBeFalsy(); + }); + }); + }); + describe('it should accept', () => { + ['1m', '2m', '1000m'].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(customDurationRt.decode(input))).toBeTruthy(); + }); }); }); }); -}); -describe('getDurationRt', () => { - const customDurationRt = getDurationRt({ min: -1 }); - describe('it should not accept', () => { - [undefined, null, '', 0, 'foo', true, false, '100', 's', 'm', '-2ms'].map( - input => { + describe('must be between 1ms and 1s', () => { + const customDurationRt = getDurationRt({ min: '1ms', max: '1s' }); + + describe('it should not accept', () => { + [ + undefined, + null, + '', + 0, + 'foo', + true, + false, + '-1s', + '0s', + '2s', + '1001ms', + '0ms', + '-1ms', + '0m', + '1m' + ].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(customDurationRt.decode(input))).toBeFalsy(); + }); + }); + }); + describe('it should return correct error message', () => { + ['-1s', '0s', '2s', '1001ms', '0ms', '-1ms', '0m', '1m'].map(input => { + it(`${JSON.stringify(input)}`, () => { + const result = customDurationRt.decode(input); + const message = PathReporter.report(result)[0]; + expect(message).toEqual('Must be between 1ms and 1s'); + expect(isRight(result)).toBeFalsy(); + }); + }); + }); + describe('it should accept', () => { + ['1s', '1ms', '50ms', '1000ms'].map(input => { it(`${JSON.stringify(input)}`, () => { - expect(isRight(customDurationRt.decode(input))).toBe(false); + expect(isRight(customDurationRt.decode(input))).toBeTruthy(); }); - } - ); + }); + }); }); + describe('must be max 1m', () => { + const customDurationRt = getDurationRt({ max: '1m' }); - describe('it should accept', () => { - ['1000ms', '2s', '3m', '1s', '-1s', '0ms'].map(input => { - it(`${JSON.stringify(input)}`, () => { - expect(isRight(customDurationRt.decode(input))).toBe(true); + describe('it should not accept', () => { + [undefined, null, '', 0, 'foo', true, false, '2m', '61s', '60001ms'].map( + input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(customDurationRt.decode(input))).toBeFalsy(); + }); + } + ); + }); + describe('it should return correct error message', () => { + ['2m', '61s', '60001ms'].map(input => { + it(`${JSON.stringify(input)}`, () => { + const result = customDurationRt.decode(input); + const message = PathReporter.report(result)[0]; + expect(message).toEqual('Must be less than 1m'); + expect(isRight(result)).toBeFalsy(); + }); + }); + }); + describe('it should accept', () => { + ['1m', '0m', '-1m', '60s', '6000ms', '1ms', '1s'].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(customDurationRt.decode(input))).toBeTruthy(); + }); }); }); }); diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.ts index b691276854fb0..cede5ed262558 100644 --- a/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.ts +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/duration_rt.ts @@ -6,32 +6,45 @@ import * as t from 'io-ts'; import { either } from 'fp-ts/lib/Either'; -import { amountAndUnitToObject } from '../amount_and_unit'; +import moment, { unitOfTime } from 'moment'; +import { amountAndUnitToObject, AmountAndUnit } from '../amount_and_unit'; +import { getRangeTypeMessage } from './get_range_type_message'; -export const DURATION_UNITS = ['ms', 's', 'm']; +function toMilliseconds({ amount, unit }: AmountAndUnit) { + return moment.duration(amount, unit as unitOfTime.Base); +} + +function amountAndUnitToMilliseconds(value?: string) { + if (value) { + const { amount, unit } = amountAndUnitToObject(value); + if (isFinite(amount) && unit) { + return toMilliseconds({ amount, unit }); + } + } +} + +export function getDurationRt({ min, max }: { min?: string; max?: string }) { + const minAsMilliseconds = amountAndUnitToMilliseconds(min) ?? -Infinity; + const maxAsMilliseconds = amountAndUnitToMilliseconds(max) ?? Infinity; + const message = getRangeTypeMessage(min, max); -export function getDurationRt({ min }: { min: number }) { return new t.Type( 'durationRt', t.string.is, (input, context) => { return either.chain(t.string.validate(input, context), inputAsString => { - const { amount, unit } = amountAndUnitToObject(inputAsString); - const amountAsInt = parseInt(amount, 10); - const isValidUnit = DURATION_UNITS.includes(unit); - const isValid = amountAsInt >= min && isValidUnit; + const inputAsMilliseconds = amountAndUnitToMilliseconds(inputAsString); + + const isValidAmount = + inputAsMilliseconds !== undefined && + inputAsMilliseconds >= minAsMilliseconds && + inputAsMilliseconds <= maxAsMilliseconds; - return isValid + return isValidAmount ? t.success(inputAsString) - : t.failure( - input, - context, - `Must have numeric amount and a valid unit (${DURATION_UNITS})` - ); + : t.failure(input, context, message); }); }, t.identity ); } - -export const durationRt = getDurationRt({ min: 1 }); diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/float_rt.test.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/float_rt.test.ts new file mode 100644 index 0000000000000..82fb8ee068b30 --- /dev/null +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/float_rt.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { floatRt } from './float_rt'; +import { isRight } from 'fp-ts/lib/Either'; + +describe('floatRt', () => { + it('does not accept empty values', () => { + expect(isRight(floatRt.decode(undefined))).toBe(false); + expect(isRight(floatRt.decode(null))).toBe(false); + expect(isRight(floatRt.decode(''))).toBe(false); + }); + + it('should only accept stringified numbers', () => { + expect(isRight(floatRt.decode('0.5'))).toBe(true); + expect(isRight(floatRt.decode(0.5))).toBe(false); + }); + + it('checks if the number falls within 0, 1', () => { + expect(isRight(floatRt.decode('0'))).toBe(true); + expect(isRight(floatRt.decode('0.5'))).toBe(true); + expect(isRight(floatRt.decode('-0.1'))).toBe(false); + expect(isRight(floatRt.decode('1.1'))).toBe(false); + expect(isRight(floatRt.decode(NaN))).toBe(false); + }); + + it('checks whether the number of decimals is 3', () => { + expect(isRight(floatRt.decode('1'))).toBe(true); + expect(isRight(floatRt.decode('0.9'))).toBe(true); + expect(isRight(floatRt.decode('0.99'))).toBe(true); + expect(isRight(floatRt.decode('0.999'))).toBe(true); + expect(isRight(floatRt.decode('0.9999'))).toBe(false); + }); +}); diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/float_rt.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/float_rt.ts new file mode 100644 index 0000000000000..4aa166f84bfe9 --- /dev/null +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/float_rt.ts @@ -0,0 +1,28 @@ +/* + * 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 t from 'io-ts'; +import { either } from 'fp-ts/lib/Either'; + +export const floatRt = new t.Type( + 'floatRt', + t.string.is, + (input, context) => { + return either.chain(t.string.validate(input, context), inputAsString => { + const inputAsFloat = parseFloat(inputAsString); + const maxThreeDecimals = + parseFloat(inputAsFloat.toFixed(3)) === inputAsFloat; + + const isValid = + inputAsFloat >= 0 && inputAsFloat <= 1 && maxThreeDecimals; + + return isValid + ? t.success(inputAsString) + : t.failure(input, context, 'Must be a number between 0.000 and 1'); + }); + }, + t.identity +); diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/get_range_type_message.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/get_range_type_message.ts new file mode 100644 index 0000000000000..5bd0fcb80c4dd --- /dev/null +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/get_range_type_message.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isFinite } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { amountAndUnitToObject } from '../amount_and_unit'; + +function getRangeType(min?: number, max?: number) { + if (isFinite(min) && isFinite(max)) { + return 'between'; + } else if (isFinite(min)) { + return 'gt'; // greater than + } else if (isFinite(max)) { + return 'lt'; // less than + } +} + +export function getRangeTypeMessage( + min?: number | string, + max?: number | string +) { + return i18n.translate('xpack.apm.agentConfig.range.errorText', { + defaultMessage: `{rangeType, select, + between {Must be between {min} and {max}} + gt {Must be greater than {min}} + lt {Must be less than {max}} + other {Must be an integer} + }`, + values: { + min, + max, + rangeType: getRangeType( + typeof min === 'string' ? amountAndUnitToObject(min).amount : min, + typeof max === 'string' ? amountAndUnitToObject(max).amount : max + ) + } + }); +} diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/integer_rt.test.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/integer_rt.test.ts index ef7fbeed4331e..a0395a4a140d9 100644 --- a/x-pack/plugins/apm/common/agent_configuration/runtime_types/integer_rt.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/integer_rt.test.ts @@ -4,43 +4,63 @@ * you may not use this file except in compliance with the Elastic License. */ -import { integerRt, getIntegerRt } from './integer_rt'; +import { getIntegerRt } from './integer_rt'; import { isRight } from 'fp-ts/lib/Either'; +import { PathReporter } from 'io-ts/lib/PathReporter'; -describe('integerRt', () => { - describe('it should not accept', () => { - [undefined, null, '', 'foo', 0, 55, NaN].map(input => { - it(`${JSON.stringify(input)}`, () => { - expect(isRight(integerRt.decode(input))).toBe(false); +describe('getIntegerRt', () => { + describe('with range', () => { + const integerRt = getIntegerRt({ + min: 0, + max: 32000 + }); + + describe('it should not accept', () => { + [NaN, undefined, null, '', 'foo', 0, 55, '-1', '-55', '33000'].map( + input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(integerRt.decode(input))).toBe(false); + }); + } + ); + }); + + describe('it should return correct error message', () => { + ['-1', '-55', '33000'].map(input => { + it(`${JSON.stringify(input)}`, () => { + const result = integerRt.decode(input); + const message = PathReporter.report(result)[0]; + expect(message).toEqual('Must be between 0 and 32000'); + expect(isRight(result)).toBeFalsy(); + }); }); }); - }); - describe('it should accept', () => { - ['-1234', '-1', '0', '1000', '32000', '100000'].map(input => { - it(`${JSON.stringify(input)}`, () => { - expect(isRight(integerRt.decode(input))).toBe(true); + describe('it should accept number between 0 and 32000', () => { + ['0', '1000', '32000'].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(integerRt.decode(input))).toBe(true); + }); }); }); }); -}); -describe('getIntegerRt', () => { - const customIntegerRt = getIntegerRt({ min: 0, max: 32000 }); - describe('it should not accept', () => { - [undefined, null, '', 'foo', 0, 55, '-1', '-55', '33000', NaN].map( - input => { + describe('without range', () => { + const integerRt = getIntegerRt(); + + describe('it should not accept', () => { + [NaN, undefined, null, '', 'foo', 0, 55].map(input => { it(`${JSON.stringify(input)}`, () => { - expect(isRight(customIntegerRt.decode(input))).toBe(false); + expect(isRight(integerRt.decode(input))).toBe(false); }); - } - ); - }); + }); + }); - describe('it should accept', () => { - ['0', '1000', '32000'].map(input => { - it(`${JSON.stringify(input)}`, () => { - expect(isRight(customIntegerRt.decode(input))).toBe(true); + describe('it should accept any number', () => { + ['-100', '-1', '0', '1000', '32000', '100000'].map(input => { + it(`${JSON.stringify(input)}`, () => { + expect(isRight(integerRt.decode(input))).toBe(true); + }); }); }); }); diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/integer_rt.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/integer_rt.ts index 6dbf175c8b4ce..adb91992f756a 100644 --- a/x-pack/plugins/apm/common/agent_configuration/runtime_types/integer_rt.ts +++ b/x-pack/plugins/apm/common/agent_configuration/runtime_types/integer_rt.ts @@ -6,8 +6,17 @@ import * as t from 'io-ts'; import { either } from 'fp-ts/lib/Either'; +import { getRangeTypeMessage } from './get_range_type_message'; + +export function getIntegerRt({ + min = -Infinity, + max = Infinity +}: { + min?: number; + max?: number; +} = {}) { + const message = getRangeTypeMessage(min, max); -export function getIntegerRt({ min, max }: { min: number; max: number }) { return new t.Type( 'integerRt', t.string.is, @@ -17,15 +26,9 @@ export function getIntegerRt({ min, max }: { min: number; max: number }) { const isValid = inputAsInt >= min && inputAsInt <= max; return isValid ? t.success(inputAsString) - : t.failure( - input, - context, - `Number must be a valid number between ${min} and ${max}` - ); + : t.failure(input, context, message); }); }, t.identity ); } - -export const integerRt = getIntegerRt({ min: -Infinity, max: Infinity }); diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/number_float_rt.test.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/number_float_rt.test.ts deleted file mode 100644 index ece229ca162fb..0000000000000 --- a/x-pack/plugins/apm/common/agent_configuration/runtime_types/number_float_rt.test.ts +++ /dev/null @@ -1,36 +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 { numberFloatRt } from './number_float_rt'; -import { isRight } from 'fp-ts/lib/Either'; - -describe('numberFloatRt', () => { - it('does not accept empty values', () => { - expect(isRight(numberFloatRt.decode(undefined))).toBe(false); - expect(isRight(numberFloatRt.decode(null))).toBe(false); - expect(isRight(numberFloatRt.decode(''))).toBe(false); - }); - - it('should only accept stringified numbers', () => { - expect(isRight(numberFloatRt.decode('0.5'))).toBe(true); - expect(isRight(numberFloatRt.decode(0.5))).toBe(false); - }); - - it('checks if the number falls within 0, 1', () => { - expect(isRight(numberFloatRt.decode('0'))).toBe(true); - expect(isRight(numberFloatRt.decode('0.5'))).toBe(true); - expect(isRight(numberFloatRt.decode('-0.1'))).toBe(false); - expect(isRight(numberFloatRt.decode('1.1'))).toBe(false); - expect(isRight(numberFloatRt.decode(NaN))).toBe(false); - }); - - it('checks whether the number of decimals is 3', () => { - expect(isRight(numberFloatRt.decode('1'))).toBe(true); - expect(isRight(numberFloatRt.decode('0.9'))).toBe(true); - expect(isRight(numberFloatRt.decode('0.99'))).toBe(true); - expect(isRight(numberFloatRt.decode('0.999'))).toBe(true); - expect(isRight(numberFloatRt.decode('0.9999'))).toBe(false); - }); -}); diff --git a/x-pack/plugins/apm/common/agent_configuration/runtime_types/number_float_rt.ts b/x-pack/plugins/apm/common/agent_configuration/runtime_types/number_float_rt.ts deleted file mode 100644 index f1890c9851a3d..0000000000000 --- a/x-pack/plugins/apm/common/agent_configuration/runtime_types/number_float_rt.ts +++ /dev/null @@ -1,36 +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 t from 'io-ts'; -import { either } from 'fp-ts/lib/Either'; - -export function getNumberFloatRt({ min, max }: { min: number; max: number }) { - return new t.Type( - 'numberFloatRt', - t.string.is, - (input, context) => { - return either.chain(t.string.validate(input, context), inputAsString => { - const inputAsFloat = parseFloat(inputAsString); - const maxThreeDecimals = - parseFloat(inputAsFloat.toFixed(3)) === inputAsFloat; - - const isValid = - inputAsFloat >= min && inputAsFloat <= max && maxThreeDecimals; - - return isValid - ? t.success(inputAsString) - : t.failure( - input, - context, - `Number must be between ${min} and ${max}` - ); - }); - }, - t.identity - ); -} - -export const numberFloatRt = getNumberFloatRt({ min: 0, max: 1 }); diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap index ea706be9f584a..4f5763dcde582 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap @@ -4,24 +4,24 @@ exports[`settingDefinitions should have correct default values 1`] = ` Array [ Object { "key": "api_request_size", + "min": "0b", "type": "bytes", "units": Array [ "b", "kb", "mb", ], - "validationError": "Please specify an integer and a unit", "validationName": "bytesRt", }, Object { "key": "api_request_time", + "min": "1ms", "type": "duration", "units": Array [ "ms", "s", "m", ], - "validationError": "Please specify an integer and a unit", "validationName": "durationRt", }, Object { @@ -84,24 +84,25 @@ Array [ }, Object { "key": "profiling_inferred_spans_min_duration", + "min": "1ms", "type": "duration", "units": Array [ "ms", "s", "m", ], - "validationError": "Please specify an integer and a unit", "validationName": "durationRt", }, Object { "key": "profiling_inferred_spans_sampling_interval", + "max": "1s", + "min": "1ms", "type": "duration", "units": Array [ "ms", "s", "m", ], - "validationError": "Please specify an integer and a unit", "validationName": "durationRt", }, Object { @@ -111,81 +112,75 @@ Array [ }, Object { "key": "server_timeout", + "min": "1ms", "type": "duration", "units": Array [ "ms", "s", "m", ], - "validationError": "Please specify an integer and a unit", "validationName": "durationRt", }, Object { "key": "span_frames_min_duration", - "min": -1, + "min": "-1ms", "type": "duration", "units": Array [ "ms", "s", "m", ], - "validationError": "Please specify an integer and a unit", "validationName": "durationRt", }, Object { "key": "stack_trace_limit", + "max": undefined, + "min": undefined, "type": "integer", - "validationError": "Must be an integer", "validationName": "integerRt", }, Object { "key": "stress_monitor_cpu_duration_threshold", + "min": "1m", "type": "duration", "units": Array [ "ms", "s", "m", ], - "validationError": "Please specify an integer and a unit", "validationName": "durationRt", }, Object { "key": "stress_monitor_gc_relief_threshold", "type": "float", - "validationError": "Must be a number between 0.000 and 1", - "validationName": "numberFloatRt", + "validationName": "floatRt", }, Object { "key": "stress_monitor_gc_stress_threshold", "type": "float", - "validationError": "Must be a number between 0.000 and 1", - "validationName": "numberFloatRt", + "validationName": "floatRt", }, Object { "key": "stress_monitor_system_cpu_relief_threshold", "type": "float", - "validationError": "Must be a number between 0.000 and 1", - "validationName": "numberFloatRt", + "validationName": "floatRt", }, Object { "key": "stress_monitor_system_cpu_stress_threshold", "type": "float", - "validationError": "Must be a number between 0.000 and 1", - "validationName": "numberFloatRt", + "validationName": "floatRt", }, Object { "key": "transaction_max_spans", "max": 32000, "min": 0, "type": "integer", - "validationError": "Must be between 0 and 32000", "validationName": "integerRt", }, Object { "key": "transaction_sample_rate", "type": "float", - "validationError": "Must be a number between 0.000 and 1", - "validationName": "numberFloatRt", + "validationName": "floatRt", }, ] `; diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index 7477238ba79ae..4ade59d489040 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -5,14 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { getIntegerRt } from '../runtime_types/integer_rt'; import { captureBodyRt } from '../runtime_types/capture_body_rt'; import { RawSettingDefinition } from './types'; -import { getDurationRt } from '../runtime_types/duration_rt'; -/* - * Settings added here will show up in the UI and will be validated on the client and server - */ export const generalSettings: RawSettingDefinition[] = [ // API Request Size { @@ -144,7 +139,7 @@ export const generalSettings: RawSettingDefinition[] = [ { key: 'span_frames_min_duration', type: 'duration', - validation: getDurationRt({ min: -1 }), + min: '-1ms', defaultValue: '5ms', label: i18n.translate('xpack.apm.agentConfig.spanFramesMinDuration.label', { defaultMessage: 'Span frames minimum duration' @@ -156,8 +151,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'In its default settings, the APM agent will collect a stack trace with every recorded span.\nWhile this is very helpful to find the exact place in your code that causes the span, collecting this stack trace does have some overhead. \nWhen setting this option to a negative value, like `-1ms`, stack traces will be collected for all spans. Setting it to a positive value, e.g. `5ms`, will limit stack trace collection to spans with durations equal to or longer than the given value, e.g. 5 milliseconds.\n\nTo disable stack trace collection for spans completely, set the value to `0ms`.' } ), - excludeAgents: ['js-base', 'rum-js', 'nodejs'], - min: -1 + excludeAgents: ['js-base', 'rum-js', 'nodejs'] }, // STACK_TRACE_LIMIT @@ -182,11 +176,8 @@ export const generalSettings: RawSettingDefinition[] = [ { key: 'transaction_max_spans', type: 'integer', - validation: getIntegerRt({ min: 0, max: 32000 }), - validationError: i18n.translate( - 'xpack.apm.agentConfig.transactionMaxSpans.errorText', - { defaultMessage: 'Must be between 0 and 32000' } - ), + min: 0, + max: 32000, defaultValue: '500', label: i18n.translate('xpack.apm.agentConfig.transactionMaxSpans.label', { defaultMessage: 'Transaction max spans' @@ -198,8 +189,6 @@ export const generalSettings: RawSettingDefinition[] = [ 'Limits the amount of spans that are recorded per transaction.' } ), - min: 0, - max: 32000, excludeAgents: ['js-base', 'rum-js'] }, diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.ts index 8786a94be096d..7869cd7d79e17 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.ts @@ -7,58 +7,75 @@ import * as t from 'io-ts'; import { sortBy } from 'lodash'; import { isRight } from 'fp-ts/lib/Either'; -import { i18n } from '@kbn/i18n'; +import { PathReporter } from 'io-ts/lib/PathReporter'; import { AgentName } from '../../../typings/es_schemas/ui/fields/agent'; import { booleanRt } from '../runtime_types/boolean_rt'; -import { integerRt } from '../runtime_types/integer_rt'; +import { getIntegerRt } from '../runtime_types/integer_rt'; import { isRumAgentName } from '../../agent_name'; -import { numberFloatRt } from '../runtime_types/number_float_rt'; -import { bytesRt, BYTE_UNITS } from '../runtime_types/bytes_rt'; -import { durationRt, DURATION_UNITS } from '../runtime_types/duration_rt'; +import { floatRt } from '../runtime_types/float_rt'; import { RawSettingDefinition, SettingDefinition } from './types'; import { generalSettings } from './general_settings'; import { javaSettings } from './java_settings'; +import { getDurationRt } from '../runtime_types/duration_rt'; +import { getBytesRt } from '../runtime_types/bytes_rt'; + +function getSettingDefaults(setting: RawSettingDefinition): SettingDefinition { + switch (setting.type) { + case 'select': + return { validation: t.string, ...setting }; -function getDefaultsByType(settingDefinition: RawSettingDefinition) { - switch (settingDefinition.type) { case 'boolean': - return { validation: booleanRt }; + return { validation: booleanRt, ...setting }; + case 'text': - return { validation: t.string }; - case 'integer': + return { validation: t.string, ...setting }; + + case 'integer': { + const { min, max } = setting; + return { - validation: integerRt, - validationError: i18n.translate( - 'xpack.apm.agentConfig.integer.errorText', - { defaultMessage: 'Must be an integer' } - ) + validation: getIntegerRt({ min, max }), + min, + max, + ...setting }; - case 'float': + } + + case 'float': { return { - validation: numberFloatRt, - validationError: i18n.translate( - 'xpack.apm.agentConfig.float.errorText', - { defaultMessage: 'Must be a number between 0.000 and 1' } - ) + validation: floatRt, + ...setting }; - case 'bytes': + } + + case 'bytes': { + const units = setting.units ?? ['b', 'kb', 'mb']; + const min = setting.min ?? '0b'; + const max = setting.max; + return { - validation: bytesRt, - units: BYTE_UNITS, - validationError: i18n.translate( - 'xpack.apm.agentConfig.bytes.errorText', - { defaultMessage: 'Please specify an integer and a unit' } - ) + validation: getBytesRt({ min, max }), + units, + min, + ...setting }; - case 'duration': + } + + case 'duration': { + const units = setting.units ?? ['ms', 's', 'm']; + const min = setting.min ?? '1ms'; + const max = setting.max; + return { - validation: durationRt, - units: DURATION_UNITS, - validationError: i18n.translate( - 'xpack.apm.agentConfig.bytes.errorText', - { defaultMessage: 'Please specify an integer and a unit' } - ) + validation: getDurationRt({ min, max }), + units, + min, + ...setting }; + } + + default: + return setting; } } @@ -91,23 +108,14 @@ export function filterByAgent(agentName?: AgentName) { }; } -export function isValid(setting: SettingDefinition, value: unknown) { - return isRight(setting.validation.decode(value)); +export function validateSetting(setting: SettingDefinition, value: unknown) { + const result = setting.validation.decode(value); + const message = PathReporter.report(result)[0]; + const isValid = isRight(result); + return { isValid, message }; } -export const settingDefinitions = sortBy( - [...generalSettings, ...javaSettings].map(def => { - const defWithDefaults = { - ...getDefaultsByType(def), - ...def - }; - - // ensure every option has validation - if (!defWithDefaults.validation) { - throw new Error(`Missing validation for ${def.key}`); - } - - return defWithDefaults as SettingDefinition; - }), +export const settingDefinitions: SettingDefinition[] = sortBy( + [...generalSettings, ...javaSettings].map(getSettingDefaults), 'key' ); diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts index 2e10c74378549..bc8f19becf053 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/java_settings.ts @@ -99,7 +99,8 @@ export const javaSettings: RawSettingDefinition[] = [ 'The minimal time required in order to determine whether the system is either currently under stress, or that the stress detected previously has been relieved. All measurements during this time must be consistent in comparison to the relevant threshold in order to detect a change of stress state. Must be at least `1m`.' } ), - includeAgents: ['java'] + includeAgents: ['java'], + min: '1m' }, { key: 'stress_monitor_system_cpu_stress_threshold', @@ -176,7 +177,9 @@ export const javaSettings: RawSettingDefinition[] = [ 'The frequency at which stack traces are gathered within a profiling session. The lower you set it, the more accurate the durations will be. This comes at the expense of higher overhead and more spans for potentially irrelevant operations. The minimal duration of a profiling-inferred span is the same as the value of this setting.' } ), - includeAgents: ['java'] + includeAgents: ['java'], + min: '1ms', + max: '1s' }, { key: 'profiling_inferred_spans_min_duration', diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/types.d.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/types.d.ts index 815b8cb3d4e83..85a454b5f256a 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/types.d.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/types.d.ts @@ -7,6 +7,9 @@ import * as t from 'io-ts'; import { AgentName } from '../../../typings/es_schemas/ui/fields/agent'; +// TODO: is it possible to get rid of `any`? +export type SettingValidation = t.Type; + interface BaseSetting { /** * UI: unique key to identify setting @@ -25,7 +28,7 @@ interface BaseSetting { category?: string; /** - * UI: + * UI: Default value set by agent */ defaultValue?: string; @@ -39,16 +42,6 @@ interface BaseSetting { */ placeholder?: string; - /** - * runtime validation of the input - */ - validation?: t.Type; - - /** - * UI: error shown when the runtime validation fails - */ - validationError?: string; - /** * Limits the setting to no agents, except those specified in `includeAgents` */ @@ -62,36 +55,41 @@ interface BaseSetting { interface TextSetting extends BaseSetting { type: 'text'; -} - -interface IntegerSetting extends BaseSetting { - type: 'integer'; - min?: number; - max?: number; -} - -interface FloatSetting extends BaseSetting { - type: 'float'; + validation?: SettingValidation; } interface SelectSetting extends BaseSetting { type: 'select'; options: Array<{ text: string; value: string }>; + validation?: SettingValidation; } interface BooleanSetting extends BaseSetting { type: 'boolean'; } +interface FloatSetting extends BaseSetting { + type: 'float'; +} + +interface IntegerSetting extends BaseSetting { + type: 'integer'; + min?: number; + max?: number; +} + interface BytesSetting extends BaseSetting { type: 'bytes'; + min?: string; + max?: string; units?: string[]; } interface DurationSetting extends BaseSetting { type: 'duration'; + min?: string; + max?: string; units?: string[]; - min?: number; } export type RawSettingDefinition = @@ -104,5 +102,8 @@ export type RawSettingDefinition = | DurationSetting; export type SettingDefinition = RawSettingDefinition & { - validation: NonNullable; + /** + * runtime validation of input + */ + validation: SettingValidation; }; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx index fcd75a05b01d9..6711fecc2376c 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SettingDefinition } from '../../../../../../../common/agent_configuration/setting_definitions/types'; -import { isValid } from '../../../../../../../common/agent_configuration/setting_definitions'; +import { validateSetting } from '../../../../../../../common/agent_configuration/setting_definitions'; import { amountAndUnitToString, amountAndUnitToObject @@ -92,12 +92,14 @@ function FormRow({ onChange( setting.key, - amountAndUnitToString({ amount: e.target.value, unit }) + amountAndUnitToString({ + amount: e.target.value, + unit + }) ) } /> @@ -137,7 +139,8 @@ export function SettingFormRow({ value?: string; onChange: (key: string, value: string) => void; }) { - const isInvalid = value != null && value !== '' && !isValid(setting, value); + const { isValid, message } = validateSetting(setting, value); + const isInvalid = value != null && value !== '' && !isValid; return ( } > - + diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx index e41bdaf0c9c09..bb3c2b3249363 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx @@ -29,7 +29,7 @@ import { AgentConfigurationIntake } from '../../../../../../../common/agent_conf import { filterByAgent, settingDefinitions, - isValid + validateSetting } from '../../../../../../../common/agent_configuration/setting_definitions'; import { saveConfig } from './saveConfig'; import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; @@ -79,7 +79,7 @@ export function SettingsPage({ // every setting must be valid for the form to be valid .every(def => { const value = newConfig.settings[def.key]; - return isValid(def, value); + return validateSetting(def, value).isValid; }) ); }, [newConfig.settings]); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 481dfffd2e3a0..da8673da67f42 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4065,7 +4065,6 @@ "xpack.apm.agentConfig.apiRequestSize.label": "API リクエストサイズ", "xpack.apm.agentConfig.apiRequestTime.description": "APM Server への HTTP リクエストを開いておく最大時間。\n\n注:この値は、APM Server の「read_timeout」設定よりも低くする必要があります。", "xpack.apm.agentConfig.apiRequestTime.label": "API リクエスト時間", - "xpack.apm.agentConfig.bytes.errorText": "整数と単位を指定してください", "xpack.apm.agentConfig.captureBody.description": "HTTP リクエストのトランザクションの場合、エージェントはオプションとしてリクエスト本文 (POST 変数など) をキャプチャすることができます。デフォルトは「off」です。", "xpack.apm.agentConfig.captureBody.label": "本文をキャプチャ", "xpack.apm.agentConfig.captureHeaders.description": "「true」に設定すると、エージェントは Cookie を含むリクエストヘッダーとレスポンスヘッダーをキャプチャします。\n\n注:これを「false」に設定すると、ネットワーク帯域幅、ディスク容量、およびオブジェクト割り当てが減少します。", @@ -4099,8 +4098,6 @@ "xpack.apm.agentConfig.editConfigTitle": "構成の編集", "xpack.apm.agentConfig.enableLogCorrelation.description": "エージェントが SLF4J のhttps://www.slf4j.org/api/org/slf4j/MDC.html[MDC] と融合してトレースログ相関を有効にすべきかどうかを指定するブール値。\n「true」に設定した場合、エージェントは現在アクティブなスパンとトランザクションの「trace.id」と「transaction.id」を MDC に設定します。\n詳細は <> をご覧ください。\n\n注:実行時にこの設定を有効にできますが、再起動しないと無効にはできません。", "xpack.apm.agentConfig.enableLogCorrelation.label": "ログ相関を有効にする", - "xpack.apm.agentConfig.float.errorText": "0.000 から 1 までの数字でなければなりません", - "xpack.apm.agentConfig.integer.errorText": "整数でなければなりません", "xpack.apm.agentConfig.logLevel.description": "エージェントのログ記録レベルを設定します", "xpack.apm.agentConfig.logLevel.label": "ログレベル", "xpack.apm.agentConfig.newConfig.description": "これで Kibana でエージェント構成を直接的に微調整できます。\n しかも、変更は APM エージェントに自動的に伝達されるので、再デプロイする必要はありません。", @@ -4150,7 +4147,6 @@ "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.description": "システム CPU 監視でシステム CPU ストレスの検出に使用するしきい値。\nシステム CPU が少なくとも「stress_monitor_cpu_duration_threshold」と同じ長さ以上の期間にわたってこのしきい値を超えると、監視機能はこれをストレス状態と見なします。", "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.label": "ストレス監視システム CPU ストレスしきい値", "xpack.apm.agentConfig.transactionMaxSpans.description": "トランザクションごとに記録される範囲を制限します。デフォルトは 500 です。", - "xpack.apm.agentConfig.transactionMaxSpans.errorText": "0 と 32000 の間でなければなりません", "xpack.apm.agentConfig.transactionMaxSpans.label": "トランザクションの最大範囲", "xpack.apm.agentConfig.transactionSampleRate.description": "デフォルトでは、エージェントはすべてのトランザクション (例えば、サービスへのリクエストなど) をサンプリングします。オーバーヘッドやストレージ要件を減らすには、サンプルレートの値を 0.0〜1.0 に設定します。全体的な時間とサンプリングされないトランザクションの結果は記録されますが、コンテキスト情報、ラベル、スパンは記録されません。", "xpack.apm.agentConfig.transactionSampleRate.label": "トランザクションのサンプルレート", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ca0e070c9bfd4..f66e9631b0168 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4066,7 +4066,6 @@ "xpack.apm.agentConfig.apiRequestSize.label": "API 请求大小", "xpack.apm.agentConfig.apiRequestTime.description": "使 APM Server 的 HTTP 请求保持开放的最大时间。\n\n注意:此值必须小于 APM Server 的 `read_timeout` 设置。", "xpack.apm.agentConfig.apiRequestTime.label": "API 请求时间", - "xpack.apm.agentConfig.bytes.errorText": "请指定整数和单位", "xpack.apm.agentConfig.captureBody.description": "有关属于 HTTP 请求的事务,代理可以选择性地捕获请求正文(例如 POST 变量)。默认为“off”。", "xpack.apm.agentConfig.captureBody.label": "捕获正文", "xpack.apm.agentConfig.captureHeaders.description": "如果设置为 `true`,代理将捕获请求和响应标头,包括 cookie。\n\n注意:将其设置为 `false` 可减少网络带宽、磁盘空间和对象分配。", @@ -4100,8 +4099,6 @@ "xpack.apm.agentConfig.editConfigTitle": "编辑配置", "xpack.apm.agentConfig.enableLogCorrelation.description": "指定是否应在 SLF4J 的 https://www.slf4j.org/api/org/slf4j/MDC.html[MDC] 中集成代理以启用跟踪-日志关联的布尔值。\n如果设置为 `true`,代理会将当前活动跨度和事务的 `trace.id` 和 `transaction.id` 设置为 MDC。\n请参阅 <> 以了解更多详情。\n\n注意:尽管允许在运行时启用此设置,但不重新启动将无法禁用。", "xpack.apm.agentConfig.enableLogCorrelation.label": "启用日志关联", - "xpack.apm.agentConfig.float.errorText": "必须是介于 0.000 和 1 之间的数字", - "xpack.apm.agentConfig.integer.errorText": "必须为整数", "xpack.apm.agentConfig.logLevel.description": "设置代理的日志记录级别", "xpack.apm.agentConfig.logLevel.label": "日志级别", "xpack.apm.agentConfig.newConfig.description": "这允许您直接在 Kibana 中微调\n 代理配置。最重要是,更改自动传播给您的 APM\n 代理,从而无需重新部署。", @@ -4151,7 +4148,6 @@ "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.description": "系统 CPU 监测用于检测系统 CPU 压力的阈值。\n如果系统 CPU 超过此阈值的持续时间至少有 `stress_monitor_cpu_duration_threshold`,\n监测会将其视为压力状态。", "xpack.apm.agentConfig.stressMonitorSystemCpuStressThreshold.label": "压力监测系统 cpu 压力阈值", "xpack.apm.agentConfig.transactionMaxSpans.description": "限制每个事务记录的跨度数量。默认值为 500。", - "xpack.apm.agentConfig.transactionMaxSpans.errorText": "必须介于 0 和 32000 之间", "xpack.apm.agentConfig.transactionMaxSpans.label": "事务最大跨度数", "xpack.apm.agentConfig.transactionSampleRate.description": "默认情况下,代理将采样每个事务(例如对服务的请求)。要减少开销和存储需要,可以将采样率设置介于 0.0 和 1.0 之间的值。我们仍记录整体时间和未采样事务的结果,但不记录上下文信息、标签和跨度。", "xpack.apm.agentConfig.transactionSampleRate.label": "事务采样率", From 55c94c8430168ccaf6d4bbf0ecb2af0a0a89f45b Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 4 May 2020 11:16:02 -0400 Subject: [PATCH 085/122] [Ingest] Add data to Overview page (#65024) --- .../common/types/rest_spec/agent.ts | 2 +- .../hooks/use_request/agents.ts | 9 + .../overview/components/agent_section.tsx | 91 ++++++++++ .../components/configuration_section.tsx | 79 +++++++++ .../components/datastream_section.tsx | 101 +++++++++++ .../components/integration_section.tsx | 78 +++++++++ .../overview/components/overview_panel.tsx | 26 +++ .../overview/components/overview_stats.tsx | 23 +++ .../sections/overview/index.tsx | 163 +----------------- 9 files changed, 417 insertions(+), 155 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_panel.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_stats.tsx diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index 64ed95db74f4c..7214611ca9122 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -152,7 +152,7 @@ export interface UpdateAgentRequest { export interface GetAgentStatusRequest { query: { - configId: string; + configId?: string; }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts index 453bcf2bd81e7..cad1791af41be 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts @@ -62,6 +62,15 @@ export function sendGetAgentStatus( }); } +export function useGetAgentStatus(query: GetAgentStatusRequest['query'], options?: RequestOptions) { + return useRequest({ + method: 'get', + path: agentRouteService.getStatusPath(), + query, + ...options, + }); +} + export function sendPutAgentReassign( agentId: string, body: PutAgentReassignRequest['body'], diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx new file mode 100644 index 0000000000000..0f6d3c5b55ce6 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx @@ -0,0 +1,91 @@ +/* + * 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 { EuiFlexItem, EuiI18nNumber } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiButtonEmpty, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { OverviewPanel } from './overview_panel'; +import { OverviewStats } from './overview_stats'; +import { useLink, useGetAgentStatus } from '../../../hooks'; +import { FLEET_PATH } from '../../../constants'; +import { Loading } from '../../fleet/components'; + +export const OverviewAgentSection = () => { + const agentStatusRequest = useGetAgentStatus({}); + + return ( + + +
    + +

    + +

    +
    + + + +
    + + {agentStatusRequest.isLoading ? ( + + ) : ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + )} + +
    +
    + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx new file mode 100644 index 0000000000000..b74cac9a62176 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexItem, EuiI18nNumber } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiButtonEmpty, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { OverviewPanel } from './overview_panel'; +import { OverviewStats } from './overview_stats'; +import { useLink, useGetDatasources } from '../../../hooks'; +import { AgentConfig } from '../../../types'; +import { AGENT_CONFIG_PATH } from '../../../constants'; +import { Loading } from '../../fleet/components'; + +export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[] }> = ({ + agentConfigs, +}) => { + const datasourcesRequest = useGetDatasources({ + page: 1, + perPage: 10000, + }); + + return ( + + +
    + +

    + +

    +
    + + + +
    + + {datasourcesRequest.isLoading ? ( + + ) : ( + <> + + + + + + + + + + + + + + )} + +
    +
    + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx new file mode 100644 index 0000000000000..7d1f0598a2767 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx @@ -0,0 +1,101 @@ +/* + * 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 { EuiFlexItem, EuiI18nNumber } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiButtonEmpty, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { OverviewPanel } from './overview_panel'; +import { OverviewStats } from './overview_stats'; +import { useLink, useGetDataStreams, useStartDeps } from '../../../hooks'; +import { Loading } from '../../fleet/components'; +import { DATA_STREAM_PATH } from '../../../constants'; + +export const OverviewDatastreamSection: React.FC = () => { + const datastreamRequest = useGetDataStreams(); + const { + data: { fieldFormats }, + } = useStartDeps(); + + const total = datastreamRequest.data?.data_streams?.length ?? 0; + let sizeBytes = 0; + const namespaces = new Set(); + if (datastreamRequest.data) { + datastreamRequest.data.data_streams.forEach(val => { + namespaces.add(val.namespace); + sizeBytes += val.size_in_bytes; + }); + } + + let size: string; + try { + const formatter = fieldFormats.getInstance('bytes'); + size = formatter.convert(sizeBytes); + } catch (e) { + size = `${sizeBytes}b`; + } + + return ( + + +
    + +

    + +

    +
    + + + +
    + + {datastreamRequest.isLoading ? ( + + ) : ( + <> + + + + + + + + + + + + + + + + {size} + + )} + +
    +
    + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx new file mode 100644 index 0000000000000..f4c122af88371 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx @@ -0,0 +1,78 @@ +/* + * 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 { EuiFlexItem, EuiI18nNumber } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiButtonEmpty, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from '@elastic/eui'; +import { OverviewPanel } from './overview_panel'; +import { OverviewStats } from './overview_stats'; +import { useLink, useGetPackages } from '../../../hooks'; +import { EPM_PATH } from '../../../constants'; +import { Loading } from '../../fleet/components'; +import { InstallationStatus } from '../../../types'; + +export const OverviewIntegrationSection: React.FC = () => { + const packagesRequest = useGetPackages(); + + const total = packagesRequest.data?.response?.length ?? 0; + const installed = + packagesRequest.data?.response?.filter(p => p.status === InstallationStatus.installed) + ?.length ?? 0; + return ( + + +
    + +

    + +

    +
    + + + +
    + + {packagesRequest.isLoading ? ( + + ) : ( + <> + + + + + + + + + + + + + + )} + +
    +
    + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_panel.tsx new file mode 100644 index 0000000000000..41d7a7a5f0bc3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_panel.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 styled from 'styled-components'; +import { EuiPanel } from '@elastic/eui'; + +export const OverviewPanel = styled(EuiPanel).attrs(props => ({ + paddingSize: 'm', +}))` + header { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid ${props => props.theme.eui.euiColorLightShade}; + margin: -${props => props.theme.eui.paddingSizes.m} -${props => props.theme.eui.paddingSizes.m} + ${props => props.theme.eui.paddingSizes.m}; + padding: ${props => props.theme.eui.paddingSizes.s} ${props => props.theme.eui.paddingSizes.m}; + } + + h2 { + padding: ${props => props.theme.eui.paddingSizes.xs} 0; + } +`; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_stats.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_stats.tsx new file mode 100644 index 0000000000000..04de22c34fe6f --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/overview_stats.tsx @@ -0,0 +1,23 @@ +/* + * 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 styled from 'styled-components'; +import { EuiDescriptionList } from '@elastic/eui'; + +export const OverviewStats = styled(EuiDescriptionList).attrs(props => ({ + compressed: true, + textStyle: 'reverse', + type: 'column', +}))` + & > * { + margin-top: ${props => props.theme.eui.paddingSizes.s} !important; + + &:first-child, + &:nth-child(2) { + margin-top: 0 !important; + } + } +`; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx index 70d8e7d6882f8..3cd778fb4f016 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/index.tsx @@ -7,14 +7,8 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import { EuiButton, - EuiButtonEmpty, EuiBetaBadge, - EuiPanel, EuiText, - EuiTitle, - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, EuiFlexGrid, EuiFlexGroup, EuiFlexItem, @@ -22,42 +16,12 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { WithHeaderLayout } from '../../layouts'; -import { useLink, useGetAgentConfigs } from '../../hooks'; +import { useGetAgentConfigs } from '../../hooks'; import { AgentEnrollmentFlyout } from '../fleet/agent_list_page/components'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../../constants'; - -const OverviewPanel = styled(EuiPanel).attrs(props => ({ - paddingSize: 'm', -}))` - header { - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: 1px solid ${props => props.theme.eui.euiColorLightShade}; - margin: -${props => props.theme.eui.paddingSizes.m} -${props => props.theme.eui.paddingSizes.m} - ${props => props.theme.eui.paddingSizes.m}; - padding: ${props => props.theme.eui.paddingSizes.s} ${props => props.theme.eui.paddingSizes.m}; - } - - h2 { - padding: ${props => props.theme.eui.paddingSizes.xs} 0; - } -`; - -const OverviewStats = styled(EuiDescriptionList).attrs(props => ({ - compressed: true, - textStyle: 'reverse', - type: 'column', -}))` - & > * { - margin-top: ${props => props.theme.eui.paddingSizes.s} !important; - - &:first-child, - &:nth-child(2) { - margin-top: 0 !important; - } - } -`; +import { OverviewAgentSection } from './components/agent_section'; +import { OverviewConfigurationSection } from './components/configuration_section'; +import { OverviewIntegrationSection } from './components/integration_section'; +import { OverviewDatastreamSection } from './components/datastream_section'; const AlphaBadge = styled(EuiBetaBadge)` vertical-align: top; @@ -135,121 +99,12 @@ export const IngestManagerOverview: React.FunctionComponent = () => { )} - - -
    - -

    - -

    -
    - - - -
    - - Total available - 999 - Installed - 1 - Updated available - 0 - -
    -
    + + - - -
    - -

    - -

    -
    - - - -
    - - Total configs - 1 - Data sources - 1 - -
    -
    + - - -
    - -

    - -

    -
    - - - -
    - - Total agents - 0 - Active - 0 - Offline - 0 - Error - 0 - -
    -
    - - - -
    - -

    - -

    -
    - - - -
    - - Data streams - 0 - Name spaces - 0 - Total size - 0 MB - -
    -
    +
    ); From 3ba268d8a59a02c2d9aec123f2a701b2ba3e3f83 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 4 May 2020 10:22:40 -0500 Subject: [PATCH 086/122] [DOCS] Reformats settings tables (#64844) * Formats settings into tables * Formatting * Formatting --- docs/settings/alert-action-settings.asciidoc | 39 +- docs/settings/apm-settings.asciidoc | 39 +- docs/settings/dev-settings.asciidoc | 16 +- .../general-infra-logs-ui-settings.asciidoc | 31 +- docs/settings/graph-settings.asciidoc | 9 +- docs/settings/i18n-settings.asciidoc | 11 +- docs/settings/ml-settings.asciidoc | 27 +- docs/settings/monitoring-settings.asciidoc | 171 ++-- docs/settings/reporting-settings.asciidoc | 223 ++--- docs/settings/security-settings.asciidoc | 122 +-- docs/settings/spaces-settings.asciidoc | 20 +- docs/settings/telemetry-settings.asciidoc | 48 +- docs/setup/settings.asciidoc | 764 +++++++++++------- 13 files changed, 924 insertions(+), 596 deletions(-) diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index d4dbe9407b7a9..547b4fdedcec6 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -5,7 +5,7 @@ Alerting and action settings ++++ -Alerts and actions are enabled by default in {kib}, but require you configure the following in order to use them: +Alerts and actions are enabled by default in {kib}, but require you configure the following in order to use them: . <>. . <>. @@ -18,27 +18,36 @@ You can configure the following settings in the `kibana.yml` file. [[general-alert-action-settings]] ==== General settings -`xpack.encryptedSavedObjects.encryptionKey`:: +[cols="2*<"] +|=== -A string of 32 or more characters used to encrypt sensitive properties on alerts and actions before they're stored in {es}. Third party credentials — such as the username and password used to connect to an SMTP service — are an example of encrypted properties. -+ -If not set, {kib} will generate a random key on startup, but all alert and action functions will be blocked. Generated keys are not allowed for alerts and actions because when a new key is generated on restart, existing encrypted data becomes inaccessible. For the same reason, alerts and actions in high-availability deployments of {kib} will behave unexpectedly if the key isn't the same on all instances of {kib}. -+ -Although the key can be specified in clear text in `kibana.yml`, it's recommended to store this key securely in the <>. +| `xpack.encryptedSavedObjects.encryptionKey` + | A string of 32 or more characters used to encrypt sensitive properties on alerts and actions before they're stored in {es}. Third party credentials — such as the username and password used to connect to an SMTP service — are an example of encrypted properties. + + + + If not set, {kib} will generate a random key on startup, but all alert and action functions will be blocked. Generated keys are not allowed for alerts and actions because when a new key is generated on restart, existing encrypted data becomes inaccessible. For the same reason, alerts and actions in high-availability deployments of {kib} will behave unexpectedly if the key isn't the same on all instances of {kib}. + + + + Although the key can be specified in clear text in `kibana.yml`, it's recommended to store this key securely in the <>. + +|=== [float] [[action-settings]] ==== Action settings -`xpack.actions.whitelistedHosts`:: -A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly whitelisted. An empty list `[]` can be used to block built-in actions from making any external connections. -+ -Note that hosts associated with built-in actions, such as Slack and PagerDuty, are not automatically whitelisted. If you are not using the default `[*]` setting, you must ensure that the corresponding endpoints are whitelisted as well. +[cols="2*<"] +|=== + +| `xpack.actions.whitelistedHosts` + | A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly whitelisted. An empty list `[]` can be used to block built-in actions from making any external connections. + + + + Note that hosts associated with built-in actions, such as Slack and PagerDuty, are not automatically whitelisted. If you are not using the default `[*]` setting, you must ensure that the corresponding endpoints are whitelisted as well. + +| `xpack.actions.enabledActionTypes` + | A list of action types that are enabled. It defaults to `[*]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, and `.webhook`. An empty list `[]` will disable all action types. + + + + Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function. -`xpack.actions.enabledActionTypes`:: -A list of action types that are enabled. It defaults to `[*]`, enabling all types. The names for built-in {kib} action types are prefixed with a `.` and include: `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, and `.webhook`. An empty list `[]` will disable all action types. -+ -Disabled action types will not appear as an option when creating new connectors, but existing connectors and actions of that type will remain in {kib} and will not function. +|=== [float] [[alert-settings]] diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index fd53c3aeb3605..8844fcd03ae9a 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -38,27 +38,42 @@ If you'd like to change any of the default values, copy and paste the relevant settings into your `kibana.yml` configuration file. Changing these settings may disable features of the APM App. -xpack.apm.enabled:: Set to `false` to disable the APM app. Defaults to `true`. +[cols="2*<"] +|=== +| `xpack.apm.enabled` + | Set to `false` to disable the APM app. Defaults to `true`. -xpack.apm.ui.enabled:: Set to `false` to hide the APM app from the menu. Defaults to `true`. +| `xpack.apm.ui.enabled` + | Set to `false` to hide the APM app from the menu. Defaults to `true`. -xpack.apm.ui.transactionGroupBucketSize:: Number of top transaction groups displayed in the APM app. Defaults to `100`. +| `xpack.apm.ui.transactionGroupBucketSize` + | Number of top transaction groups displayed in the APM app. Defaults to `100`. -xpack.apm.ui.maxTraceItems:: Maximum number of child items displayed when viewing trace details. Defaults to `1000`. +| `xpack.apm.ui.maxTraceItems` + | Maximum number of child items displayed when viewing trace details. Defaults to `1000`. -apm_oss.indexPattern:: The index pattern used for integrations with Machine Learning and Query Bar. -It must match all apm indices. Defaults to `apm-*`. +| `apm_oss.indexPattern` + | The index pattern used for integrations with Machine Learning and Query Bar. + It must match all apm indices. Defaults to `apm-*`. -apm_oss.errorIndices:: Matcher for all {apm-server-ref}/error-indices.html[error indices]. Defaults to `apm-*`. +| `apm_oss.errorIndices` + | Matcher for all {apm-server-ref}/error-indices.html[error indices]. Defaults to `apm-*`. -apm_oss.onboardingIndices:: Matcher for all onboarding indices. Defaults to `apm-*`. +| `apm_oss.onboardingIndices` + | Matcher for all onboarding indices. Defaults to `apm-*`. -apm_oss.spanIndices:: Matcher for all {apm-server-ref}/span-indices.html[span indices]. Defaults to `apm-*`. +| `apm_oss.spanIndices` + | Matcher for all {apm-server-ref}/span-indices.html[span indices]. Defaults to `apm-*`. -apm_oss.transactionIndices:: Matcher for all {apm-server-ref}/transaction-indices.html[transaction indices]. Defaults to `apm-*`. +| `apm_oss.transactionIndices` + | Matcher for all {apm-server-ref}/transaction-indices.html[transaction indices]. Defaults to `apm-*`. -apm_oss.metricsIndices:: Matcher for all {apm-server-ref}/metricset-indices.html[metrics indices]. Defaults to `apm-*`. +| `apm_oss.metricsIndices` + | Matcher for all {apm-server-ref}/metricset-indices.html[metrics indices]. Defaults to `apm-*`. -apm_oss.sourcemapIndices:: Matcher for all {apm-server-ref}/sourcemap-indices.html[source map indices]. Defaults to `apm-*`. +| `apm_oss.sourcemapIndices` + | Matcher for all {apm-server-ref}/sourcemap-indices.html[source map indices]. Defaults to `apm-*`. + +|=== // end::general-apm-settings[] diff --git a/docs/settings/dev-settings.asciidoc b/docs/settings/dev-settings.asciidoc index 436f169b82ca3..c43b96a8668e0 100644 --- a/docs/settings/dev-settings.asciidoc +++ b/docs/settings/dev-settings.asciidoc @@ -12,12 +12,20 @@ They are enabled by default. [[grok-settings]] ==== Grok Debugger settings -`xpack.grokdebugger.enabled`:: -Set to `true` (default) to enable the <>. +[cols="2*<"] +|=== +| `xpack.grokdebugger.enabled` + | Set to `true` to enable the <>. Defaults to `true`. + +|=== [float] [[profiler-settings]] ==== {searchprofiler} Settings -`xpack.searchprofiler.enabled`:: -Set to `true` (default) to enable the <>. +[cols="2*<"] +|=== +| `xpack.searchprofiler.enabled` + | Set to `true` to enable the <>. Defaults to `true`. + +|=== diff --git a/docs/settings/general-infra-logs-ui-settings.asciidoc b/docs/settings/general-infra-logs-ui-settings.asciidoc index 7b32372a1f59a..2a9d4df1ff43c 100644 --- a/docs/settings/general-infra-logs-ui-settings.asciidoc +++ b/docs/settings/general-infra-logs-ui-settings.asciidoc @@ -1,17 +1,30 @@ -`xpack.infra.enabled`:: Set to `false` to disable the Logs and Metrics app plugin {kib}. Defaults to `true`. +[cols="2*<"] +|=== +| `xpack.infra.enabled` + | Set to `false` to disable the Logs and Metrics app plugin {kib}. Defaults to `true`. -`xpack.infra.sources.default.logAlias`:: Index pattern for matching indices that contain log data. Defaults to `filebeat-*,kibana_sample_data_logs*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. +| `xpack.infra.sources.default.logAlias` + | Index pattern for matching indices that contain log data. Defaults to `filebeat-*,kibana_sample_data_logs*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. -`xpack.infra.sources.default.metricAlias`:: Index pattern for matching indices that contain Metricbeat data. Defaults to `metricbeat-*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. +| `xpack.infra.sources.default.metricAlias` + | Index pattern for matching indices that contain Metricbeat data. Defaults to `metricbeat-*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. -`xpack.infra.sources.default.fields.timestamp`:: Timestamp used to sort log entries. Defaults to `@timestamp`. +| `xpack.infra.sources.default.fields.timestamp` + | Timestamp used to sort log entries. Defaults to `@timestamp`. -`xpack.infra.sources.default.fields.message`:: Fields used to display messages in the Logs app. Defaults to `['message', '@message']`. +| `xpack.infra.sources.default.fields.message` + | Fields used to display messages in the Logs app. Defaults to `['message', '@message']`. -`xpack.infra.sources.default.fields.tiebreaker`:: Field used to break ties between two entries with the same timestamp. Defaults to `_doc`. +| `xpack.infra.sources.default.fields.tiebreaker` + | Field used to break ties between two entries with the same timestamp. Defaults to `_doc`. -`xpack.infra.sources.default.fields.host`:: Field used to identify hosts. Defaults to `host.name`. +| `xpack.infra.sources.default.fields.host` + | Field used to identify hosts. Defaults to `host.name`. -`xpack.infra.sources.default.fields.container`:: Field used to identify Docker containers. Defaults to `container.id`. +| `xpack.infra.sources.default.fields.container` + | Field used to identify Docker containers. Defaults to `container.id`. -`xpack.infra.sources.default.fields.pod`:: Field used to identify Kubernetes pods. Defaults to `kubernetes.pod.uid`. +| `xpack.infra.sources.default.fields.pod` + | Field used to identify Kubernetes pods. Defaults to `kubernetes.pod.uid`. + +|=== diff --git a/docs/settings/graph-settings.asciidoc b/docs/settings/graph-settings.asciidoc index 7e597362b1cfc..8ccff21a26f74 100644 --- a/docs/settings/graph-settings.asciidoc +++ b/docs/settings/graph-settings.asciidoc @@ -10,5 +10,10 @@ You do not need to configure any settings to use the {graph-features}. [float] [[general-graph-settings]] ==== General graph settings -`xpack.graph.enabled`:: -Set to `false` to disable the {graph-features}. + +[cols="2*<"] +|=== +| `xpack.graph.enabled` + | Set to `false` to disable the {graph-features}. + +|=== diff --git a/docs/settings/i18n-settings.asciidoc b/docs/settings/i18n-settings.asciidoc index 4fe466bcb4580..6d92e74f17cb2 100644 --- a/docs/settings/i18n-settings.asciidoc +++ b/docs/settings/i18n-settings.asciidoc @@ -9,10 +9,7 @@ You do not need to configure any settings to run Kibana in English. ==== General i18n Settings `i18n.locale`:: -Kibana currently supports the following locales: -+ -- English - `en` (default) -- Chinese - `zh-CN` -- Japanese - `ja-JP` - - + {kib} supports the following locales: + * English - `en` (default) + * Chinese - `zh-CN` + * Japanese - `ja-JP` diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 36578c909f513..24e38e73bca9b 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -11,12 +11,25 @@ enabled by default. [[general-ml-settings-kb]] ==== General {ml} settings -`xpack.ml.enabled`:: -Set to `true` (default) to enable {kib} {ml-features}. + -+ -If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} -instance. If `xpack.ml.enabled` is set to `true` in `elasticsearch.yml`, however, -you can still use the {ml} APIs. To disable {ml} entirely, see the -{ref}/ml-settings.html[{es} {ml} settings]. +[cols="2*<"] +|=== +| `xpack.ml.enabled` + | Set to `true` (default) to enable {kib} {ml-features}. + + + + If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} + instance. If `xpack.ml.enabled` is set to `true` in `elasticsearch.yml`, however, + you can still use the {ml} APIs. To disable {ml} entirely, see the + {ref}/ml-settings.html[{es} {ml} settings]. +|=== +[[data-visualizer-settings]] +==== {data-viz} settings + +[cols="2*<"] +|=== +| `xpack.ml.file_data_visualizer.max_file_size` + | Sets the file size limit when importing data in the {data-viz}. The default + value is `100MB`. The highest supported value for this setting is `1GB`. + +|=== diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 6645f49029a51..f180f2c3ecc97 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -29,45 +29,49 @@ For more information, see [[monitoring-general-settings]] ==== General monitoring settings -`monitoring.enabled`:: -Set to `true` (default) to enable the {monitor-features} in {kib}. Unlike the -`monitoring.ui.enabled` setting, when this setting is `false`, the -monitoring back-end does not run and {kib} stats are not sent to the monitoring -cluster. - -`monitoring.ui.elasticsearch.hosts`:: -Specifies the location of the {es} cluster where your monitoring data is stored. -By default, this is the same as `elasticsearch.hosts`. This setting enables -you to use a single {kib} instance to search and visualize data in your -production cluster as well as monitor data sent to a dedicated monitoring -cluster. - -`monitoring.ui.elasticsearch.username`:: -Specifies the username used by {kib} monitoring to establish a persistent connection -in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} -monitoring cluster. - -Every other request performed by the Stack Monitoring UI to the monitoring {es} -cluster uses the authenticated user's credentials, which must be the same on -both the {es} monitoring cluster and the {es} production cluster. - -If not set, {kib} uses the value of the `elasticsearch.username` setting. - -`monitoring.ui.elasticsearch.password`:: -Specifies the password used by {kib} monitoring to establish a persistent connection -in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} -monitoring cluster. - -Every other request performed by the Stack Monitoring UI to the monitoring {es} -cluster uses the authenticated user's credentials, which must be the same on -both the {es} monitoring cluster and the {es} production cluster. - -If not set, {kib} uses the value of the `elasticsearch.password` setting. - -`monitoring.ui.elasticsearch.pingTimeout`:: -Specifies the time in milliseconds to wait for {es} to respond to internal -health checks. By default, it matches the `elasticsearch.pingTimeout` setting, -which has a default value of `30000`. +[cols="2*<"] +|=== +| `monitoring.enabled` + | Set to `true` (default) to enable the {monitor-features} in {kib}. Unlike the + `monitoring.ui.enabled` setting, when this setting is `false`, the + monitoring back-end does not run and {kib} stats are not sent to the monitoring + cluster. + +| `monitoring.ui.elasticsearch.hosts` + | Specifies the location of the {es} cluster where your monitoring data is stored. + By default, this is the same as `elasticsearch.hosts`. This setting enables + you to use a single {kib} instance to search and visualize data in your + production cluster as well as monitor data sent to a dedicated monitoring + cluster. + +| `monitoring.ui.elasticsearch.username` + | Specifies the username used by {kib} monitoring to establish a persistent connection + in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} + monitoring cluster. + + + + Every other request performed by the Stack Monitoring UI to the monitoring {es} + cluster uses the authenticated user's credentials, which must be the same on + both the {es} monitoring cluster and the {es} production cluster. + + + + If not set, {kib} uses the value of the `elasticsearch.username` setting. + +| `monitoring.ui.elasticsearch.password` + | Specifies the password used by {kib} monitoring to establish a persistent connection + in {kib} to the {es} monitoring cluster and to verify licensing status on the {es} + monitoring cluster. + + + + Every other request performed by the Stack Monitoring UI to the monitoring {es} + cluster uses the authenticated user's credentials, which must be the same on + both the {es} monitoring cluster and the {es} production cluster. + + + + If not set, {kib} uses the value of the `elasticsearch.password` setting. + +| `monitoring.ui.elasticsearch.pingTimeout` + | Specifies the time in milliseconds to wait for {es} to respond to internal + health checks. By default, it matches the `elasticsearch.pingTimeout` setting, + which has a default value of `30000`. + +|=== [float] [[monitoring-collection-settings]] @@ -75,15 +79,18 @@ which has a default value of `30000`. These settings control how data is collected from {kib}. -`monitoring.kibana.collection.enabled`:: -Set to `true` (default) to enable data collection from the {kib} NodeJS server -for {kib} Dashboards to be featured in the Monitoring. +[cols="2*<"] +|=== +| `monitoring.kibana.collection.enabled` + | Set to `true` (default) to enable data collection from the {kib} NodeJS server + for {kib} Dashboards to be featured in the Monitoring. -`monitoring.kibana.collection.interval`:: -Specifies the number of milliseconds to wait in between data sampling on the -{kib} NodeJS server for the metrics that are displayed in the {kib} dashboards. -Defaults to `10000` (10 seconds). +| `monitoring.kibana.collection.interval` + | Specifies the number of milliseconds to wait in between data sampling on the + {kib} NodeJS server for the metrics that are displayed in the {kib} dashboards. + Defaults to `10000` (10 seconds). +|=== [float] [[monitoring-ui-settings]] @@ -94,27 +101,31 @@ However, the defaults work best in most circumstances. For more information about configuring {kib}, see {kibana-ref}/settings.html[Setting Kibana Server Properties]. -`monitoring.ui.elasticsearch.logFetchCount`:: -Specifies the number of log entries to display in the Monitoring UI. Defaults to -`10`. The maximum value is `50`. +[cols="2*<"] +|=== +| `monitoring.ui.elasticsearch.logFetchCount` + | Specifies the number of log entries to display in the Monitoring UI. Defaults to + `10`. The maximum value is `50`. -`monitoring.ui.max_bucket_size`:: -Specifies the number of term buckets to return out of the overall terms list when -performing terms aggregations to retrieve index and node metrics. For more -information about the `size` parameter, see -{ref}/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-size[Terms Aggregation]. -Defaults to `10000`. +| `monitoring.ui.max_bucket_size` + | Specifies the number of term buckets to return out of the overall terms list when + performing terms aggregations to retrieve index and node metrics. For more + information about the `size` parameter, see + {ref}/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-size[Terms Aggregation]. + Defaults to `10000`. -`monitoring.ui.min_interval_seconds`:: -Specifies the minimum number of seconds that a time bucket in a chart can -represent. Defaults to 10. If you modify the -`monitoring.ui.collection.interval` in `elasticsearch.yml`, use the same -value in this setting. +| `monitoring.ui.min_interval_seconds` + | Specifies the minimum number of seconds that a time bucket in a chart can + represent. Defaults to 10. If you modify the + `monitoring.ui.collection.interval` in `elasticsearch.yml`, use the same + value in this setting. -`monitoring.ui.enabled`:: -Set to `false` to hide the Monitoring UI in {kib}. The monitoring back-end -continues to run as an agent for sending {kib} stats to the monitoring -cluster. Defaults to `true`. +| `monitoring.ui.enabled` + | Set to `false` to hide the Monitoring UI in {kib}. The monitoring back-end + continues to run as an agent for sending {kib} stats to the monitoring + cluster. Defaults to `true`. + +|=== [float] [[monitoring-ui-cgroup-settings]] @@ -125,18 +136,20 @@ better decisions about your container performance, rather than guessing based on the overall machine performance. If you are not running your applications in a container, then Cgroup statistics are not useful. -`monitoring.ui.container.elasticsearch.enabled`:: - -For {es} clusters that are running in containers, this setting changes the -*Node Listing* to display the CPU utilization based on the reported Cgroup -statistics. It also adds the calculated Cgroup CPU utilization to the -*Node Overview* page instead of the overall operating system's CPU -utilization. Defaults to `false`. - -`monitoring.ui.container.logstash.enabled`:: - -For {ls} nodes that are running in containers, this setting -changes the {ls} *Node Listing* to display the CPU utilization -based on the reported Cgroup statistics. It also adds the -calculated Cgroup CPU utilization to the {ls} node detail -pages instead of the overall operating system’s CPU utilization. Defaults to `false`. +[cols="2*<"] +|=== +| `monitoring.ui.container.elasticsearch.enabled` + | For {es} clusters that are running in containers, this setting changes the + *Node Listing* to display the CPU utilization based on the reported Cgroup + statistics. It also adds the calculated Cgroup CPU utilization to the + *Node Overview* page instead of the overall operating system's CPU + utilization. Defaults to `false`. + +| `monitoring.ui.container.logstash.enabled` + | For {ls} nodes that are running in containers, this setting + changes the {ls} *Node Listing* to display the CPU utilization + based on the reported Cgroup statistics. It also adds the + calculated Cgroup CPU utilization to the {ls} node detail + pages instead of the overall operating system’s CPU utilization. Defaults to `false`. + +|=== diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 9a45fb9ab1d0c..7c50dbf542d0d 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -14,45 +14,54 @@ You can configure `xpack.reporting` settings in your `kibana.yml` to: [float] [[general-reporting-settings]] ==== General reporting settings -[[xpack-enable-reporting]]`xpack.reporting.enabled`:: -Set to `false` to disable the {report-features}. -`xpack.reporting.encryptionKey`:: -Set to any text string. By default, Kibana will generate a random key when it -starts, which will cause pending reports to fail after restart. Configure this -setting to preserve the same key across multiple restarts and multiple instances of Kibana. +[cols="2*<"] +|=== +| [[xpack-enable-reporting]]`xpack.reporting.enabled` + | Set to `false` to disable the {report-features}. + +| `xpack.reporting.encryptionKey` + | Set to any text string. By default, {kib} will generate a random key when it + starts, which will cause pending reports to fail after restart. Configure this + setting to preserve the same key across multiple restarts and multiple instances of {kib}. + +|=== [float] [[reporting-kibana-server-settings]] -==== Kibana server settings +==== {kib} server settings -Reporting opens the {kib} web interface in a server process to generate -screenshots of {kib} visualizations. In most cases, the default settings -will work and you don't need to configure Reporting to communicate with {kib}. +Reporting opens the {kib} web interface in a server process to generate +screenshots of {kib} visualizations. In most cases, the default settings +will work and you don't need to configure Reporting to communicate with {kib}. However, if your client connections must go through a reverse-proxy -to access {kib}, Reporting configuration must have the proxy port, protocol, +to access {kib}, Reporting configuration must have the proxy port, protocol, and hostname set in the `xpack.reporting.kibanaServer.*` settings. -[NOTE] +[NOTE] ==== -If a reverse-proxy carries encrypted traffic from end-user -clients back to a {kib} server, the proxy port, protocol, and hostname -in Reporting settings must be valid for the encryption that the Reporting -browser will receive. Encrypted communications will fail if there are +If a reverse-proxy carries encrypted traffic from end-user +clients back to a {kib} server, the proxy port, protocol, and hostname +in Reporting settings must be valid for the encryption that the Reporting +browser will receive. Encrypted communications will fail if there are mismatches in the host information between the request and the certificate on the server. Configuring the `xpack.reporting.kibanaServer` settings to point to a -proxy host requires that the Kibana server has network access to the proxy. +proxy host requires that the {kib} server has network access to the proxy. ==== -`xpack.reporting.kibanaServer.port`:: -The port for accessing Kibana, if different from the `server.port` value. +[cols="2*<"] +|=== +| `xpack.reporting.kibanaServer.port` + | The port for accessing {kib}, if different from the `server.port` value. + +| `xpack.reporting.kibanaServer.protocol` + | The protocol for accessing {kib}, typically `http` or `https`. -`xpack.reporting.kibanaServer.protocol`:: -The protocol for accessing Kibana, typically `http` or `https`. +| `xpack.reporting.kibanaServer.hostname` + | The hostname for accessing {kib}, if different from the `server.host` value. -`xpack.reporting.kibanaServer.hostname`:: -The hostname for accessing {kib}, if different from the `server.host` value. +|=== [NOTE] ============ @@ -68,55 +77,67 @@ because, in the Reporting browser, it becomes an automatic redirect to `"0.0.0.0 ==== Background job settings Reporting generates reports in the background and jobs are coordinated using documents -in Elasticsearch. Depending on how often you generate reports and the overall number of +in {es}. Depending on how often you generate reports and the overall number of reports, you might need to change the following settings. -`xpack.reporting.queue.indexInterval`:: -How often the index that stores reporting jobs rolls over to a new index. -Valid values are `year`, `month`, `week`, `day`, and `hour`. Defaults to `week`. +[cols="2*<"] +|=== +| `xpack.reporting.queue.indexInterval` + | How often the index that stores reporting jobs rolls over to a new index. + Valid values are `year`, `month`, `week`, `day`, and `hour`. Defaults to `week`. -`xpack.reporting.queue.pollEnabled`:: -Set to `true` (default) to enable the Kibana instance to to poll the index for -pending jobs and claim them for execution. Setting this to `false` allows the -Kibana instance to only add new jobs to the reporting queue, list jobs, and -provide the downloads to completed report through the UI. +| `xpack.reporting.queue.pollEnabled` + | Set to `true` (default) to enable the {kib} instance to to poll the index for + pending jobs and claim them for execution. Setting this to `false` allows the + {kib} instance to only add new jobs to the reporting queue, list jobs, and + provide the downloads to completed report through the UI. + +|=== [NOTE] ============ -Running multiple instances of Kibana in a cluster for load balancing of +Running multiple instances of {kib} in a cluster for load balancing of reporting requires identical values for `xpack.reporting.encryptionKey` and, if security is enabled, `xpack.security.encryptionKey`. ============ -`xpack.reporting.queue.pollInterval`:: -Specifies the number of milliseconds that the reporting poller waits between polling the -index for any pending Reporting jobs. Defaults to `3000` (3 seconds). +[cols="2*<"] +|=== +| `xpack.reporting.queue.pollInterval` + | Specifies the number of milliseconds that the reporting poller waits between polling the + index for any pending Reporting jobs. Defaults to `3000` (3 seconds). + +| [[xpack-reporting-q-timeout]] `xpack.reporting.queue.timeout` + | How long each worker has to produce a report. If your machine is slow or under + heavy load, you might need to increase this timeout. Specified in milliseconds. + If a Reporting job execution time goes over this time limit, the job will be + marked as a failure and there will not be a download available. + Defaults to `120000` (two minutes). -[[xpack-reporting-q-timeout]]`xpack.reporting.queue.timeout`:: -How long each worker has to produce a report. If your machine is slow or under -heavy load, you might need to increase this timeout. Specified in milliseconds. -If a Reporting job execution time goes over this time limit, the job will be -marked as a failure and there will not be a download available. -Defaults to `120000` (two minutes). +|=== [float] [[reporting-capture-settings]] ==== Capture settings -Reporting works by capturing screenshots from Kibana. The following settings +Reporting works by capturing screenshots from {kib}. The following settings control the capturing process. -`xpack.reporting.capture.timeouts.openUrl`:: -How long to allow the Reporting browser to wait for the initial data of the -Kibana page to load. Defaults to `30000` (30 seconds). +[cols="2*<"] +|=== +| `xpack.reporting.capture.timeouts.openUrl` + | How long to allow the Reporting browser to wait for the initial data of the + {kib} page to load. Defaults to `30000` (30 seconds). + +| `xpack.reporting.capture.timeouts.waitForElements` + | How long to allow the Reporting browser to wait for the visualization panels to + load on the {kib} page. Defaults to `30000` (30 seconds). -`xpack.reporting.capture.timeouts.waitForElements`:: -How long to allow the Reporting browser to wait for the visualization panels to -load on the Kibana page. Defaults to `30000` (30 seconds). +| `xpack.reporting.capture.timeouts.renderComplete` + | How long to allow the Reporting browser to wait for each visualization to + signal that it is done renderings. Defaults to `30000` (30 seconds). -`xpack.reporting.capture.timeouts.renderComplete`:: -How long to allow the Reporting brwoser to wait for each visualization to -signal that it is done renderings. Defaults to `30000` (30 seconds). +|=== [NOTE] ============ @@ -126,20 +147,24 @@ capturing the page with a screenshot. As a result, a download will be available, but there will likely be errors in the visualizations in the report. ============ -`xpack.reporting.capture.maxAttempts`:: -If capturing a report fails for any reason, Kibana will re-attempt othe reporting -job, as many times as this setting. Defaults to `3`. +[cols="2*<"] +|=== +| `xpack.reporting.capture.maxAttempts` + | If capturing a report fails for any reason, {kib} will re-attempt other reporting + job, as many times as this setting. Defaults to `3`. -`xpack.reporting.capture.loadDelay`:: -When visualizations are not evented, this is the amount of time before -taking a screenshot. All visualizations that ship with Kibana are evented, so this -setting should not have much effect. If you are seeing empty images instead of -visualizations, try increasing this value. -Defaults to `3000` (3 seconds). +| `xpack.reporting.capture.loadDelay` + | When visualizations are not evented, this is the amount of time before + taking a screenshot. All visualizations that ship with {kib} are evented, so this + setting should not have much effect. If you are seeing empty images instead of + visualizations, try increasing this value. + Defaults to `3000` (3 seconds). -[[xpack-reporting-browser]]`xpack.reporting.capture.browser.type`:: -Specifies the browser to use to capture screenshots. This setting exists for -backward compatibility. The only valid option is `chromium`. +| [[xpack-reporting-browser]] `xpack.reporting.capture.browser.type` + | Specifies the browser to use to capture screenshots. This setting exists for + backward compatibility. The only valid option is `chromium`. + +|=== [float] [[reporting-chromium-settings]] @@ -147,47 +172,59 @@ backward compatibility. The only valid option is `chromium`. When `xpack.reporting.capture.browser.type` is set to `chromium` (default) you can also specify the following settings. -`xpack.reporting.capture.browser.chromium.disableSandbox`:: -Elastic recommends that you research the feasibility of enabling unprivileged user namespaces. -See Chromium Sandbox for additional information. Defaults to false for all operating systems except Debian, -Red Hat Linux, and CentOS which use true +[cols="2*<"] +|=== +| `xpack.reporting.capture.browser.chromium.disableSandbox` + | It is recommended that you research the feasibility of enabling unprivileged user namespaces. + See Chromium Sandbox for additional information. Defaults to false for all operating systems except Debian, + Red Hat Linux, and CentOS which use true. -`xpack.reporting.capture.browser.chromium.proxy.enabled`:: -Enables the proxy for Chromium to use. When set to `true`, you must also specify the -`xpack.reporting.capture.browser.chromium.proxy.server` setting. -Defaults to `false` +| `xpack.reporting.capture.browser.chromium.proxy.enabled` + | Enables the proxy for Chromium to use. When set to `true`, you must also specify the + `xpack.reporting.capture.browser.chromium.proxy.server` setting. + Defaults to `false`. -`xpack.reporting.capture.browser.chromium.proxy.server`:: -The uri for the proxy server. Providing the username and password for the proxy server via the uri is not supported. +| `xpack.reporting.capture.browser.chromium.proxy.server` + | The uri for the proxy server. Providing the username and password for the proxy server via the uri is not supported. -`xpack.reporting.capture.browser.chromium.proxy.bypass`:: -An array of hosts that should not go through the proxy server and should use a direct connection instead. -Examples of valid entries are "elastic.co", "*.elastic.co", ".elastic.co", ".elastic.co:5601" +| `xpack.reporting.capture.browser.chromium.proxy.bypass` + | An array of hosts that should not go through the proxy server and should use a direct connection instead. + Examples of valid entries are "elastic.co", "*.elastic.co", ".elastic.co", ".elastic.co:5601". +|=== [float] [[reporting-csv-settings]] ==== CSV settings -[[xpack-reporting-csv]]`xpack.reporting.csv.maxSizeBytes`:: -The maximum size of a CSV file before being truncated. This setting exists to prevent -large exports from causing performance and storage issues. -Defaults to `10485760` (10mB) + +[cols="2*<"] +|=== +| [[xpack-reporting-csv]] `xpack.reporting.csv.maxSizeBytes` + | The maximum size of a CSV file before being truncated. This setting exists to prevent + large exports from causing performance and storage issues. + Defaults to `10485760` (10mB). + +|=== [float] [[reporting-advanced-settings]] ==== Advanced settings -`xpack.reporting.index`:: -Reporting uses a weekly index in Elasticsearch to store the reporting job and -the report content. The index is automatically created if it does not already -exist. Configure this to a unique value, beginning with `.reporting-`, for every -Kibana instance that has a unique `kibana.index` setting. Defaults to `.reporting` +[cols="2*<"] +|=== +| `xpack.reporting.index` + | Reporting uses a weekly index in {es} to store the reporting job and + the report content. The index is automatically created if it does not already + exist. Configure this to a unique value, beginning with `.reporting-`, for every + {kib} instance that has a unique `kibana.index` setting. Defaults to `.reporting`. + +| `xpack.reporting.roles.allow` + | Specifies the roles in addition to superusers that can use reporting. + Defaults to `[ "reporting_user" ]`. + -`xpack.reporting.roles.allow`:: -Specifies the roles in addition to superusers that can use reporting. -Defaults to `[ "reporting_user" ]` -+ --- -NOTE: Each user has access to only their own reports. +|=== --- +[NOTE] +============ +Each user has access to only their own reports. +============ diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 16d68a7759f77..8f6905d643139 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -12,55 +12,83 @@ You do not need to configure any additional settings to use the [[general-security-settings]] ==== General security settings -`xpack.security.enabled`:: -By default, {kib} automatically detects whether to enable the -{security-features} based on the license and whether {es} {security-features} -are enabled. -+ -Do not set this to `false`; it disables the login form, user and role management -screens, and authorization using <>. To disable -{security-features} entirely, see -{ref}/security-settings.html[{es} security settings]. - -`xpack.security.audit.enabled`:: -Set to `true` to enable audit logging for security events. By default, it is set -to `false`. For more details see <>. +[cols="2*<"] +|=== +| `xpack.security.enabled` + | By default, {kib} automatically detects whether to enable the + {security-features} based on the license and whether {es} {security-features} + are enabled. + + + + Do not set this to `false`; it disables the login form, user and role management + screens, and authorization using <>. To disable + {security-features} entirely, see + {ref}/security-settings.html[{es} security settings]. + +| `xpack.security.audit.enabled` + | Set to `true` to enable audit logging for security events. By default, it is set + to `false`. For more details see <>. + +|=== [float] [[security-ui-settings]] ==== User interface security settings -You can configure the following settings in the `kibana.yml` file: - -`xpack.security.cookieName`:: -Sets the name of the cookie used for the session. The default value is `"sid"`. - -`xpack.security.encryptionKey`:: -An arbitrary string of 32 characters or more that is used to encrypt credentials -in a cookie. It is crucial that this key is not exposed to users of {kib}. By -default, a value is automatically generated in memory. If you use that default -behavior, all sessions are invalidated when {kib} restarts. -In addition, high-availability deployments of {kib} will behave unexpectedly -if this setting isn't the same for all instances of {kib}. - -`xpack.security.secureCookies`:: -Sets the `secure` flag of the session cookie. The default value is `false`. It -is automatically set to `true` if `server.ssl.enabled` is set to `true`. Set -this to `true` if SSL is configured outside of {kib} (for example, you are -routing requests through a load balancer or proxy). - -`xpack.security.session.idleTimeout`:: -Sets the session duration. The format is a string of `[ms|s|m|h|d|w|M|Y]` -(e.g. '70ms', '5s', '3d', '1Y'). By default, sessions stay active until the -browser is closed. When this is set to an explicit idle timeout, closing the -browser still requires the user to log back in to {kib}. - -`xpack.security.session.lifespan`:: -Sets the maximum duration, also known as "absolute timeout". The format is a -string of `[ms|s|m|h|d|w|M|Y]` (e.g. '70ms', '5s', '3d', '1Y'). By default, -a session can be renewed indefinitely. When this value is set, a session will end -once its lifespan is exceeded, even if the user is not idle. NOTE: if `idleTimeout` -is not set, this setting will still cause sessions to expire. - -`xpack.security.loginAssistanceMessage`:: -Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. +You can configure the following settings in the `kibana.yml` file. + +[cols="2*<"] +|=== +| `xpack.security.cookieName` + | Sets the name of the cookie used for the session. The default value is `"sid"`. + +| `xpack.security.encryptionKey` + | An arbitrary string of 32 characters or more that is used to encrypt credentials + in a cookie. It is crucial that this key is not exposed to users of {kib}. By + default, a value is automatically generated in memory. If you use that default + behavior, all sessions are invalidated when {kib} restarts. + In addition, high-availability deployments of {kib} will behave unexpectedly + if this setting isn't the same for all instances of {kib}. + +| `xpack.security.secureCookies` + | Sets the `secure` flag of the session cookie. The default value is `false`. It + is automatically set to `true` if `server.ssl.enabled` is set to `true`. Set + this to `true` if SSL is configured outside of {kib} (for example, you are + routing requests through a load balancer or proxy). + +| `xpack.security.session.idleTimeout` + | Sets the session duration. By default, sessions stay active until the + browser is closed. When this is set to an explicit idle timeout, closing the + browser still requires the user to log back in to {kib}. + +|=== + +[TIP] +============ +The format is a string of `[ms|s|m|h|d|w|M|Y]` +(e.g. '70ms', '5s', '3d', '1Y'). +============ + +[cols="2*<"] +|=== + +| `xpack.security.session.lifespan` + | Sets the maximum duration, also known as "absolute timeout". By default, + a session can be renewed indefinitely. When this value is set, a session will end + once its lifespan is exceeded, even if the user is not idle. NOTE: if `idleTimeout` + is not set, this setting will still cause sessions to expire. + +|=== + +[TIP] +============ +The format is a +string of `[ms|s|m|h|d|w|M|Y]` (e.g. '70ms', '5s', '3d', '1Y'). +============ + +[cols="2*<"] +|=== + +| `xpack.security.loginAssistanceMessage` + | Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. + +|=== diff --git a/docs/settings/spaces-settings.asciidoc b/docs/settings/spaces-settings.asciidoc index bb0a15b29a087..bda5f00f762cd 100644 --- a/docs/settings/spaces-settings.asciidoc +++ b/docs/settings/spaces-settings.asciidoc @@ -5,18 +5,22 @@ Spaces settings ++++ -By default, Spaces is enabled in Kibana, and you can secure Spaces using +By default, Spaces is enabled in Kibana, and you can secure Spaces using roles when Security is enabled. [float] [[spaces-settings]] ==== Spaces settings -`xpack.spaces.enabled`:: -Set to `true` (default) to enable Spaces in {kib}. +[cols="2*<"] +|=== +| `xpack.spaces.enabled` + | Set to `true` (default) to enable Spaces in {kib}. -`xpack.spaces.maxSpaces`:: -The maximum amount of Spaces that can be used with this instance of Kibana. Some operations -in Kibana return all spaces using a single `_search` from Elasticsearch, so this must be -set lower than the `index.max_result_window` in Elasticsearch. -Defaults to `1000`. \ No newline at end of file +| `xpack.spaces.maxSpaces` + | The maximum amount of Spaces that can be used with this instance of {kib}. Some operations + in {kib} return all spaces using a single `_search` from {es}, so this must be + set lower than the `index.max_result_window` in {es}. + Defaults to `1000`. + +|=== diff --git a/docs/settings/telemetry-settings.asciidoc b/docs/settings/telemetry-settings.asciidoc index ad5f53ad879f8..33f167b13b310 100644 --- a/docs/settings/telemetry-settings.asciidoc +++ b/docs/settings/telemetry-settings.asciidoc @@ -8,7 +8,7 @@ By default, Usage Collection (also known as Telemetry) is enabled. This helps us learn about the {kib} features that our users are most interested in, so we can focus our efforts on making them even better. -You can control whether this data is sent from the {kib} servers, or if it should be sent +You can control whether this data is sent from the {kib} servers, or if it should be sent from the user's browser, in case a firewall is blocking the connections from the server. Additionally, you can decide to completely disable this feature either in the config file or in {kib} via *Management > Kibana > Advanced Settings > Usage Data*. See our https://www.elastic.co/legal/privacy-statement[Privacy Statement] to learn more. @@ -17,22 +17,30 @@ See our https://www.elastic.co/legal/privacy-statement[Privacy Statement] to lea [[telemetry-general-settings]] ==== General telemetry settings -`telemetry.enabled`:: *Default: true*. -Set to `true` to send cluster statistics to Elastic. Reporting your -cluster statistics helps us improve your user experience. Your data is never -shared with anyone. Set to `false` to disable statistics reporting from any -browser connected to the {kib} instance. - -`telemetry.sendUsageFrom`:: *Default: 'browser'*. -Set to `'server'` to report the cluster statistics from the {kib} server. -If the server fails to connect to our endpoint at https://telemetry.elastic.co/, it assumes -it is behind a firewall and falls back to `'browser'` to send it from users' browsers -when they are navigating through {kib}. - -`telemetry.optIn`:: *Default: true*. -Set to `true` to automatically opt into reporting cluster statistics. You can also opt out through -*Advanced Settings* in {kib}. - -`telemetry.allowChangingOptInStatus`:: *Default: true*. -Set to `true` to allow overwriting the `telemetry.optIn` setting via the {kib} UI. -Note: When `false`, `telemetry.optIn` must be `true`. To disable telemetry and not allow users to change that parameter, use `telemetry.enabled`. +[cols="2*<"] +|=== +| `telemetry.enabled` + | Set to `true` to send cluster statistics to Elastic. Reporting your + cluster statistics helps us improve your user experience. Your data is never + shared with anyone. Set to `false` to disable statistics reporting from any + browser connected to the {kib} instance. Defaults to `true`. + +| `telemetry.sendUsageFrom` + | Set to `'server'` to report the cluster statistics from the {kib} server. + If the server fails to connect to our endpoint at https://telemetry.elastic.co/, it assumes + it is behind a firewall and falls back to `'browser'` to send it from users' browsers + when they are navigating through {kib}. Defaults to 'browser'. + +| `telemetry.optIn` + | Set to `true` to automatically opt into reporting cluster statistics. You can also opt out through + *Advanced Settings* in {kib}. Defaults to `true`. + +| `telemetry.allowChangingOptInStatus` + | Set to `true` to allow overwriting the `telemetry.optIn` setting via the {kib} UI. Defaults to `true`. + + +|=== + +[NOTE] +============ +When `false`, `telemetry.optIn` must be `true`. To disable telemetry and not allow users to change that parameter, use `telemetry.enabled`. +============ diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 41fe8d337c03b..cc662af08b8f1 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -1,7 +1,7 @@ [[settings]] -== Configuring Kibana +== Configuring {kib} -The Kibana server reads properties from the `kibana.yml` file on startup. The +The {kib} server reads properties from the `kibana.yml` file on startup. The location of this file differs depending on how you installed {kib}. For example, if you installed {kib} from an archive distribution (`.tar.gz` or `.zip`), by default it is in `$KIBANA_HOME/config`. By default, with package distributions @@ -11,444 +11,622 @@ The default host and port settings configure {kib} to run on `localhost:5601`. T variety of other options. Finally, environment variables can be injected into configuration using `${MY_ENV_VAR}` syntax. -.Kibana configuration settings +[cols="2*<"] +|=== -`console.enabled:`:: *Default: true* Set to false to disable Console. Toggling -this will cause the server to regenerate assets on the next startup, which may -cause a delay before pages start being served. +| `console.enabled:` + | Toggling this causes the server to regenerate assets on the next startup, +which may cause a delay before pages start being served. +Set to `false` to disable Console. *Default: `true`* -`cpu.cgroup.path.override:`:: Override for cgroup cpu path when mounted in a -manner that is inconsistent with `/proc/self/cgroup` +| `cpu.cgroup.path.override:` + | Override for cgroup cpu path when mounted in a +manner that is inconsistent with `/proc/self/cgroup`. -`cpuacct.cgroup.path.override:`:: Override for cgroup cpuacct path when mounted -in a manner that is inconsistent with `/proc/self/cgroup` +| `cpuacct.cgroup.path.override:` + | Override for cgroup cpuacct path when mounted +in a manner that is inconsistent with `/proc/self/cgroup`. -`csp.rules:`:: A template -https://w3c.github.io/webappsec-csp/[content-security-policy] that disables -certain unnecessary and potentially insecure capabilities in the browser. We -strongly recommend that you keep the default CSP rules that ship with Kibana. +| `csp.rules:` + | A https://w3c.github.io/webappsec-csp/[content-security-policy] template +that disables certain unnecessary and potentially insecure capabilities in +the browser. It is strongly recommended that you keep the default CSP rules +that ship with {kib}. -`csp.strict:`:: *Default: `true`* Blocks access to Kibana to any browser that -does not enforce even rudimentary CSP rules. In practice, this will disable +| `csp.strict:` + | Blocks {kib} access to any browser that +does not enforce even rudimentary CSP rules. In practice, this disables support for older, less safe browsers like Internet Explorer. -See <> for more information. - -`csp.warnLegacyBrowsers:`:: *Default: `true`* Shows a warning message after -loading Kibana to any browser that does not enforce even rudimentary CSP rules, -though Kibana is still accessible. This configuration is effectively ignored -when `csp.strict` is enabled. - -`elasticsearch.customHeaders:`:: *Default: `{}`* Header names and values to send -to Elasticsearch. Any custom headers cannot be overwritten by client-side -headers, regardless of the `elasticsearch.requestHeadersWhitelist` configuration. - -`elasticsearch.hosts:`:: *Default: `[ "http://localhost:9200" ]`* The URLs of the {es} instances to use for all your queries. All nodes -listed here must be on the same cluster. +For more information, refer to <>. +*Default: `true`* + +| `csp.warnLegacyBrowsers:` + | Shows a warning message after loading {kib} to any browser that does not +enforce even rudimentary CSP rules, though {kib} is still accessible. This +configuration is effectively ignored when `csp.strict` is enabled. +*Default: `true`* + +| `elasticsearch.customHeaders:` + | Header names and values to send to {es}. Any custom headers cannot be +overwritten by client-side headers, regardless of the +`elasticsearch.requestHeadersWhitelist` configuration. *Default: `{}`* + +| `elasticsearch.hosts:` + | The URLs of the {es} instances to use for all your queries. All nodes +listed here must be on the same cluster. *Default: `[ "http://localhost:9200" ]`* + -To enable SSL/TLS for outbound connections to {es}, use the `https` protocol in this setting. - -`elasticsearch.logQueries:`:: *Default: `false`* Logs queries sent to -Elasticsearch. Requires `logging.verbose` set to `true`. This is useful for -seeing the query DSL generated by applications that currently do not have an -inspector, for example Timelion and Monitoring. - -`elasticsearch.pingTimeout:`:: -*Default: the value of the `elasticsearch.requestTimeout` setting* Time in -milliseconds to wait for Elasticsearch to respond to pings. - -`elasticsearch.preserveHost:`:: *Default: true* When this setting’s value is -true, Kibana uses the hostname specified in the `server.host` setting. When the -value of this setting is `false`, Kibana uses the hostname of the host that -connects to this Kibana instance. - -`elasticsearch.requestHeadersWhitelist:`:: *Default: `[ 'authorization' ]`* List -of Kibana client-side headers to send to Elasticsearch. To send *no* client-side -headers, set this value to [] (an empty list). -Removing the `authorization` header from being whitelisted means that you cannot -use <> in Kibana. - -`elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait -for responses from the back end or Elasticsearch. This value must be a positive -integer. - -`elasticsearch.shardTimeout:`:: *Default: 30000* Time in milliseconds for -Elasticsearch to wait for responses from shards. Set to 0 to disable. - -`elasticsearch.sniffInterval:`:: *Default: false* Time in milliseconds between -requests to check Elasticsearch for an updated list of nodes. - -`elasticsearch.sniffOnStart:`:: *Default: false* Attempt to find other -Elasticsearch nodes on startup. - -`elasticsearch.sniffOnConnectionFault:`:: *Default: false* Update the list of -Elasticsearch nodes immediately following a connection fault. - -`elasticsearch.ssl.alwaysPresentCertificate:`:: *Default: false* Controls {kib}'s behavior in regard to presenting a client certificate when -requested by {es}. This setting applies to all outbound SSL/TLS connections to {es}, including requests that are proxied for end users. -+ -WARNING: If {es} uses certificates to authenticate end users with a PKI realm and `elasticsearch.ssl.alwaysPresentCertificate` is `true`, -proxied requests may be executed as the identity that is tied to the {kib} server. - -`elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:`:: Paths to a PEM-encoded X.509 client certificate and its corresponding -private key. These are used by {kib} to authenticate itself when making outbound SSL/TLS connections to {es}. For this setting to take -effect, the `xpack.security.http.ssl.client_authentication` setting in {es} must be also be set to `"required"` or `"optional"` to request a -client certificate from {kib}. +To enable SSL/TLS for outbound connections to {es}, use the `https` protocol +in this setting. + +| `elasticsearch.logQueries:` + | Log queries sent to {es}. Requires `logging.verbose` set to `true`. +This is useful for seeing the query DSL generated by applications that +currently do not have an inspector, for example Timelion and Monitoring. +*Default: `false`* + +| `elasticsearch.pingTimeout:` + | Time in milliseconds to wait for {es} to respond to pings. +*Default: the value of the `elasticsearch.requestTimeout` setting* + +| `elasticsearch.preserveHost:` + | When the value is `true`, {kib} uses the hostname specified in the +`server.host` setting. When the value is `false`, {kib} uses +the hostname of the host that connects to this {kib} instance. *Default: `true`* + +| `elasticsearch.requestHeadersWhitelist:` + | List of {kib} client-side headers to send to {es}. To send *no* client-side +headers, set this value to [] (an empty list). Removing the `authorization` +header from being whitelisted means that you cannot use +<> in {kib}. +*Default: `[ 'authorization' ]`* + +| `elasticsearch.requestTimeout:` + | Time in milliseconds to wait for responses from the back end or {es}. +This value must be a positive integer. *Default: `30000`* + +| `elasticsearch.shardTimeout:` + | Time in milliseconds for {es} to wait for responses from shards. +Set to 0 to disable. *Default: `30000`* + +| `elasticsearch.sniffInterval:` + | Time in milliseconds between requests to check {es} for an updated list of +nodes. *Default: `false`* + +| `elasticsearch.sniffOnStart:` + | Attempt to find other {es} nodes on startup. *Default: `false`* + +| `elasticsearch.sniffOnConnectionFault:` + | Update the list of {es} nodes immediately following a connection fault. +*Default: `false`* + +| `elasticsearch.ssl.alwaysPresentCertificate:` + | Controls {kib} behavior in regard to presenting a client certificate when +requested by {es}. This setting applies to all outbound SSL/TLS connections +to {es}, including requests that are proxied for end users. *Default: `false`* + +|=== + +[WARNING] +============ +When {es} uses certificates to authenticate end users with a PKI realm +and `elasticsearch.ssl.alwaysPresentCertificate` is `true`, +proxied requests may be executed as the identity that is tied to the {kib} +server. +============ + +[cols="2*<"] +|=== + +| `elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:` + | Paths to a PEM-encoded X.509 client certificate and its corresponding +private key. These are used by {kib} to authenticate itself when making +outbound SSL/TLS connections to {es}. For this setting to take effect, the +`xpack.security.http.ssl.client_authentication` setting in {es} must be also +be set to `"required"` or `"optional"` to request a client certificate from +{kib}. + +|=== + +[NOTE] +============ +These settings cannot be used in conjunction with `elasticsearch.ssl.keystore.path`. +============ + +[cols="2*<"] +|=== + +| `elasticsearch.ssl.certificateAuthorities:` + | Paths to one or more PEM-encoded X.509 certificate authority (CA) +certificates, which make up a trusted certificate chain for {es}. This chain is +used by {kib} to establish trust when making outbound SSL/TLS connections to +{es}. + -NOTE: These settings cannot be used in conjunction with `elasticsearch.ssl.keystore.path`. - -`elasticsearch.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates which make up a -trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections to {es}. +In addition to this setting, trusted certificates may be specified via +`elasticsearch.ssl.keystore.path` and/or `elasticsearch.ssl.truststore.path`. + +| `elasticsearch.ssl.keyPassphrase:` + | The password that decrypts the private key that is specified +via `elasticsearch.ssl.key`. This value is optional, as the key may not be +encrypted. + +| `elasticsearch.ssl.keystore.path:` + | Path to a PKCS#12 keystore that contains an X.509 client certificate and it's +corresponding private key. These are used by {kib} to authenticate itself when +making outbound SSL/TLS connections to {es}. For this setting, you must also set +the `xpack.security.http.ssl.client_authentication` setting in {es} to +`"required"` or `"optional"` to request a client certificate from {kib}. + -In addition to this setting, trusted certificates may be specified via `elasticsearch.ssl.keystore.path` and/or +If the keystore contains any additional certificates, they are used as a +trusted certificate chain for {es}. This chain is used by {kib} to establish +trust when making outbound SSL/TLS connections to {es}. In addition to this +setting, trusted certificates may be specified via +`elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.truststore.path`. -`elasticsearch.ssl.keyPassphrase:`:: The password that will be used to decrypt the private key that is specified via -`elasticsearch.ssl.key`. This value is optional, as the key may not be encrypted. +|=== -`elasticsearch.ssl.keystore.path:`:: Path to a PKCS#12 keystore that contains an X.509 client certificate and its corresponding private key. -These are used by {kib} to authenticate itself when making outbound SSL/TLS connections to {es}. For this setting to take effect, the -`xpack.security.http.ssl.client_authentication` setting in {es} must also be set to `"required"` or `"optional"` to request a client -certificate from {kib}. -+ --- -If the keystore contains any additional certificates, those will be used as a trusted certificate chain for {es}. This chain is used by -{kib} to establish trust when making outbound SSL/TLS connections to {es}. In addition to this setting, trusted certificates may be -specified via `elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.truststore.path`. +[NOTE] +============ +This setting cannot be used in conjunction with +`elasticsearch.ssl.certificate` or `elasticsearch.ssl.key`. +============ -NOTE: This setting cannot be used in conjunction with `elasticsearch.ssl.certificate` or `elasticsearch.ssl.key`. --- +[cols="2*<"] +|=== -`elasticsearch.ssl.keystore.password:`:: The password that will be used to decrypt the keystore that is specified via -`elasticsearch.ssl.keystore.path`. If the keystore has no password, leave this unset. If the keystore has an empty password, set this to +| `elasticsearch.ssl.keystore.password:` + | The password that decrypts the keystore specified via +`elasticsearch.ssl.keystore.path`. If the keystore has no password, leave this +as blank. If the keystore has an empty password, set this to `""`. -`elasticsearch.ssl.truststore.path:`:: Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates -which make up a trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections -to {es}. +| `elasticsearch.ssl.truststore.path:`:: + | Path to a PKCS#12 trust store that contains one or more X.509 certificate +authority (CA) certificates, which make up a trusted certificate chain for +{es}. This chain is used by {kib} to establish trust when making outbound +SSL/TLS connections to {es}. + -In addition to this setting, trusted certificates may be specified via `elasticsearch.ssl.certificateAuthorities` and/or +In addition to this setting, trusted certificates may be specified via +`elasticsearch.ssl.certificateAuthorities` and/or `elasticsearch.ssl.keystore.path`. -`elasticsearch.ssl.truststore.password:`:: The password that will be used to decrypt the trust store specified via -`elasticsearch.ssl.truststore.path`. If the trust store has no password, leave this unset. If the trust store has an empty password, set -this to `""`. - -`elasticsearch.ssl.verificationMode:`:: *Default: `"full"`* Controls the verification of the server certificate that {kib} receives when -making an outbound SSL/TLS connection to {es}. Valid values are `"full"`, `"certificate"`, and `"none"`. Using `"full"` will perform -hostname verification, using `"certificate"` will skip hostname verification, and using `"none"` will skip verification entirely. - -`elasticsearch.startupTimeout:`:: *Default: 5000* Time in milliseconds to wait -for Elasticsearch at Kibana startup before retrying. - -`elasticsearch.username:` and `elasticsearch.password:`:: If your Elasticsearch -is protected with basic authentication, these settings provide the username and -password that the Kibana server uses to perform maintenance on the Kibana index -at startup. Your Kibana users still need to authenticate with Elasticsearch, -which is proxied through the Kibana server. - -`interpreter.enableInVisualize`:: *Default: true* Enables use of interpreter in -Visualize. - -`kibana.defaultAppId:`:: *Default: "home"* The default application to load. - -`kibana.index:`:: *Default: ".kibana"* Kibana uses an index in Elasticsearch to -store saved searches, visualizations and dashboards. Kibana creates a new index -if the index doesn’t already exist. If you configure a custom index, the name must -be lowercase, and conform to {es} {ref}/indices-create-index.html[index name limitations]. - -`kibana.autocompleteTimeout:`:: *Default: "1000"* Time in milliseconds to wait -for autocomplete suggestions from Elasticsearch. This value must be a whole number -greater than zero. - -`kibana.autocompleteTerminateAfter:`:: *Default: "100000"* Maximum number of -documents loaded by each shard to generate autocomplete suggestions. This value -must be a whole number greater than zero. - -`logging.dest:`:: *Default: `stdout`* Enables you specify a file where Kibana -stores log output. - -`logging.json:`:: *Default: false* Logs output as JSON. When set to `true`, the -logs will be formatted as JSON strings that include timestamp, log level, context, message -text and any other metadata that may be associated with the log message itself. -If `logging.dest.stdout` is set and there is no interactive terminal ("TTY"), this setting -will default to `true`. - -`logging.quiet:`:: *Default: false* Set the value of this setting to `true` to -suppress all logging output other than error messages. - -`logging.rotate:`:: [experimental] Specifies the options for the logging rotate feature. +|`elasticsearch.ssl.truststore.password:` + | The password that decrypts the trust store specified via +`elasticsearch.ssl.truststore.path`. If the trust store has no password, +leave this as blank. If the trust store has an empty password, set this to `""`. + +| `elasticsearch.ssl.verificationMode:` + | Controls the verification of the server certificate that {kib} receives when +making an outbound SSL/TLS connection to {es}. Valid values are `"full"`, +`"certificate"`, and `"none"`. Using `"full"` performs hostname verification, +using `"certificate"` skips hostname verification, and using `"none"` skips +verification entirely. *Default: `"full"`* + +| `elasticsearch.startupTimeout:` + | Time in milliseconds to wait for {es} at {kib} startup before retrying. +*Default: `5000`* + +| `elasticsearch.username:` and `elasticsearch.password:` + | If your {es} is protected with basic authentication, these settings provide +the username and password that the {kib} server uses to perform maintenance +on the {kib} index at startup. {kib} users still need to authenticate with +{es}, which is proxied through the {kib} server. + +| `interpreter.enableInVisualize` + | Enables use of interpreter in Visualize. *Default: `true`* + +| `kibana.defaultAppId:` + | The default application to load. *Default: `"home"`* + +| `kibana.index:` + | {kib} uses an index in {es} to store saved searches, visualizations, and +dashboards. {kib} creates a new index if the index doesn’t already exist. +If you configure a custom index, the name must be lowercase, and conform to the +{es} {ref}/indices-create-index.html[index name limitations]. +*Default: `".kibana"`* + +| `kibana.autocompleteTimeout:` + | Time in milliseconds to wait for autocomplete suggestions from {es}. +This value must be a whole number greater than zero. *Default: `"1000"`* + +| `kibana.autocompleteTerminateAfter:` + | Maximum number of documents loaded by each shard to generate autocomplete +suggestions. This value must be a whole number greater than zero. +*Default: `"100000"`* + +| `logging.dest:` + | Enables you to specify a file where {kib} stores log output. +*Default: `stdout`* + +| `logging.json:` + | Logs output as JSON. When set to `true`, the logs are formatted as JSON +strings that include timestamp, log level, context, message text, and any other +metadata that may be associated with the log message. +When `logging.dest.stdout` is set, and there is no interactive terminal ("TTY"), +this setting defaults to `true`. *Default: `false`* + +| `logging.quiet:` + | Set the value of this setting to `true` to suppress all logging output other +than error messages. *Default: `false`* + +| `logging.rotate:` + | experimental[] Specifies the options for the logging rotate feature. When not defined, all the sub options defaults would be applied. The following example shows a valid logging rotate configuration: -+ + +|=== + +[source,text] -- - logging.rotate: - enabled: true - everyBytes: 10485760 - keepFiles: 10 + logging.rotate: + enabled: true + everyBytes: 10485760 + keepFiles: 10 -- -`logging.rotate.enabled:`:: [experimental] *Default: false* Set the value of this setting to `true` to +[cols="2*<"] +|=== + +| `logging.rotate.enabled:` + | experimental[] Set the value of this setting to `true` to enable log rotation. If you do not have a `logging.dest` set that is different from `stdout` -that feature would not take any effect. +that feature would not take any effect. *Default: `false`* -`logging.rotate.everyBytes:`:: [experimental] *Default: 10485760* The maximum size of a log file (that is `not an exact` limit). After the +| `logging.rotate.everyBytes:` + | experimental[] The maximum size of a log file (that is `not an exact` limit). After the limit is reached, a new log file is generated. The default size limit is 10485760 (10 MB) and -this option should be in the range of 1048576 (1 MB) to 1073741824 (1 GB). +this option should be in the range of 1048576 (1 MB) to 1073741824 (1 GB). *Default: `10485760`* -`logging.rotate.keepFiles:`:: [experimental] *Default: 7* The number of most recent rotated log files to keep +| `logging.rotate.keepFiles:` + | experimental[] The number of most recent rotated log files to keep on disk. Older files are deleted during log rotation. The default value is 7. The `logging.rotate.keepFiles` -option has to be in the range of 2 to 1024 files. +option has to be in the range of 2 to 1024 files. *Default: `7`* -`logging.rotate.pollingInterval:`:: [experimental] *Default: 10000* The number of milliseconds for the polling strategy in case -the `logging.rotate.usePolling` is enabled. That option has to be in the range of 5000 to 3600000 milliseconds. +| `logging.rotate.pollingInterval:` + | experimental[] The number of milliseconds for the polling strategy in case +the `logging.rotate.usePolling` is enabled. `logging.rotate.usePolling` must be in the 5000 to 3600000 millisecond range. *Default: `10000`* -`logging.rotate.usePolling:`:: [experimental] *Default: false* By default we try to understand the best way to monitoring +| `logging.rotate.usePolling:` + | experimental[] By default we try to understand the best way to monitoring the log file and warning about it. Please be aware there are some systems where watch api is not accurate. In those cases, in order to get the feature working, -the `polling` method could be used enabling that option. +the `polling` method could be used enabling that option. *Default: `false`* -`logging.silent:`:: *Default: false* Set the value of this setting to `true` to -suppress all logging output. +| `logging.silent:` + | Set the value of this setting to `true` to +suppress all logging output. *Default: `false`* -`logging.timezone`:: *Default: UTC* Set to the canonical timezone id -(for example, `America/Los_Angeles`) to log events using that timezone. A list of timezones can -be referenced at https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. +| `logging.timezone` + | Set to the canonical timezone ID +(for example, `America/Los_Angeles`) to log events using that timezone. For a +list of timezones, refer to https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. *Default: `UTC`* -[[logging-verbose]]`logging.verbose:`:: *Default: false* Set the value of this -setting to `true` to log all events, including system usage information and all -requests. Supported on Elastic Cloud Enterprise. +| [[logging-verbose]] `logging.verbose:` + | Set to `true` to log all events, including system usage information and all +requests. Supported on {ece}. *Default: `false`* -`map.includeElasticMapsService:`:: *Default: true* -Set to false to disable connections to Elastic Maps Service. +| `map.includeElasticMapsService:` + | Set to `false` to disable connections to Elastic Maps Service. When `includeElasticMapsService` is turned off, only the vector layers configured by `map.regionmap` -and the tile layer configured by `map.tilemap.url` will be available in <>. +and the tile layer configured by `map.tilemap.url` are available in <>. *Default: `true`* -`map.proxyElasticMapsServiceInMaps:`:: *Default: false* -Set to true to proxy all <> Elastic Maps Service requests through the Kibana server. +| `map.proxyElasticMapsServiceInMaps:` + | Set to `true` to proxy all <> Elastic Maps Service +requests through the {kib} server. *Default: `false`* -[[regionmap-settings]] `map.regionmap:`:: Specifies additional vector layers for +| [[regionmap-settings]] `map.regionmap:` + | Specifies additional vector layers for use in <> 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 (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. -[[region-map-configuration-example]] +{kib}, the server needs to be CORS-enabled so {kib} can download the file. The following example shows a valid region map configuration. -+ + +|=== + +[source,text] -- - map - includeElasticMapsService: false - regionmap: - layers: - - name: "Departments of France" - url: "http://my.cors.enabled.server.org/france_departements.geojson" - attribution: "INRAP" - fields: - - name: "department" - description: "Full department name" - - name: "INSEE" - description: "INSEE numeric identifier" +map.regionmap: + includeElasticMapsService: false + layers: + - name: "Departments of France" + url: "http://my.cors.enabled.server.org/france_departements.geojson" + attribution: "INRAP" + fields: + - name: "department" + description: "Full department name" + - name: "INSEE" + description: "INSEE numeric identifier" -- -[[regionmap-ES-map]]`map.includeElasticMapsService:`:: Turns on or off -whether layers from the Elastic Maps Service should be included in the vector -layer option list. Supported on Elastic Cloud Enterprise. By turning this off, +[cols="2*<"] +|=== + +| [[regionmap-ES-map]] `map.includeElasticMapsService:` + | Turns on or off whether layers from the Elastic Maps Service should be included in the vector +layer option list. Supported on {ece}. By turning this off, only the layers that are configured here will be included. The default is `true`. This also affects whether tile-service from the Elastic Maps Service will be available. -[[regionmap-attribution]]`map.regionmap.layers[].attribution:`:: Optional. -References the originating source of the geojson file. Supported on {ece}. +| [[regionmap-attribution]] `map.regionmap.layers[].attribution:` + | Optional. References the originating source of the geojson file. +Supported on {ece}. -[[regionmap-fields]]`map.regionmap.layers[].fields[]:`:: Mandatory. Each layer +| [[regionmap-fields]] `map.regionmap.layers[].fields[]:` + | Mandatory. Each layer can contain multiple fields to indicate what properties from the geojson -features you wish to expose. This <> shows how to define multiple -properties. Supported on {ece}. +features you wish to expose. Supported on {ece}. The following shows how to define multiple +properties: + +|=== -[[regionmap-field-description]]`map.regionmap.layers[].fields[].description:`:: -Mandatory. The human readable text that is shown under the Options tab when +[source,text] +-- +map.regionmap: + includeElasticMapsService: false + layers: + - name: "Departments of France" + url: "http://my.cors.enabled.server.org/france_departements.geojson" + attribution: "INRAP" + fields: + - name: "department" + description: "Full department name" + - name: "INSEE" + description: "INSEE numeric identifier" +-- + +[cols="2*<"] +|=== + +| [[regionmap-field-description]] `map.regionmap.layers[].fields[].description:` + | Mandatory. The human readable text that is shown under the Options tab when building the Region Map visualization. Supported on {ece}. -[[regionmap-field-name]]`map.regionmap.layers[].fields[].name:`:: Mandatory. +| [[regionmap-field-name]] `map.regionmap.layers[].fields[].name:` + | Mandatory. This value is used to do an inner-join between the document stored in -Elasticsearch and the geojson file. For example, if the field in the geojson is -called `Location` and has city names, there must be a field in Elasticsearch -that holds the same values that Kibana can then use to lookup for the geoshape +{es} and the geojson file. For example, if the field in the geojson is +called `Location` and has city names, there must be a field in {es} +that holds the same values that {kib} can then use to lookup for the geoshape data. Supported on {ece}. -[[regionmap-name]]`map.regionmap.layers[].name:`:: Mandatory. A description of +| [[regionmap-name]] `map.regionmap.layers[].name:` + | Mandatory. A description of the map being provided. Supported on {ece}. -[[regionmap-url]]`map.regionmap.layers[].url:`:: Mandatory. The location of the +| [[regionmap-url]] `map.regionmap.layers[].url:` + | Mandatory. The location of the geojson file as provided by a webserver. Supported on {ece}. -[[tilemap-settings]] `map.tilemap.options.attribution:`:: +| [[tilemap-settings]] `map.tilemap.options.attribution:` + | The map attribution string. Supported on {ece}. *Default: `"© [Elastic Maps Service](https://www.elastic.co/elastic-maps-service)"`* -The map attribution string. Supported on {ece}. -[[tilemap-max-zoom]]`map.tilemap.options.maxZoom:`:: *Default: 10* The maximum -zoom level. Supported on {ece}. +| [[tilemap-max-zoom]] `map.tilemap.options.maxZoom:` + | The maximum zoom level. Supported on {ece}. *Default: `10`* -[[tilemap-min-zoom]]`map.tilemap.options.minZoom:`:: *Default: 1* The minimum -zoom level. Supported on {ece}. +| [[tilemap-min-zoom]] `map.tilemap.options.minZoom:` + | The minimum zoom level. Supported on {ece}. *Default: `1`* -[[tilemap-subdomains]]`map.tilemap.options.subdomains:`:: An array of subdomains +| [[tilemap-subdomains]] `map.tilemap.options.subdomains:` + | An array of subdomains used by the tile service. Specify the position of the subdomain the URL with the token `{s}`. Supported on {ece}. -[[tilemap-url]]`map.tilemap.url:`:: The URL to the tileservice that Kibana uses +| [[tilemap-url]] `map.tilemap.url:` + | The URL to the tileservice that {kib} uses to display map tiles in tilemap visualizations. Supported on {ece}. By default, -Kibana reads this url from an external metadata service, but users can still +{kib} reads this URL from an external metadata service, but users can override this parameter to use their own Tile Map Service. For example: `"https://tiles.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana"` -`newsfeed.enabled:` :: *Default: `true`* Controls whether to enable the newsfeed -system for the Kibana UI notification center. Set to `false` to disable the -newsfeed system. +| `newsfeed.enabled:` + | Controls whether to enable the newsfeed +system for the {kib} UI notification center. Set to `false` to disable the +newsfeed system. *Default: `true`* -`path.data:`:: *Default: `data`* The path where Kibana stores persistent data -not saved in Elasticsearch. +| `path.data:` + | The path where {kib} stores persistent data +not saved in {es}. *Default: `data`* -`pid.file:`:: Specifies the path where Kibana creates the process ID file. +| `pid.file:` + | Specifies the path where {kib} creates the process ID file. -`ops.interval:`:: *Default: 5000* Set the interval in milliseconds to sample -system and process performance metrics. The minimum value is 100. +| `ops.interval:` + | Set the interval in milliseconds to sample +system and process performance metrics. The minimum value is 100. *Default: `5000`* -`server.basePath:`:: Enables you to specify a path to mount Kibana at if you are -running behind a proxy. Use the `server.rewriteBasePath` setting to tell Kibana +| `server.basePath:` + | Enables you to specify a path to mount {kib} at if you are +running behind a proxy. Use the `server.rewriteBasePath` setting to tell {kib} if it should remove the basePath from requests it receives, and to prevent a deprecation warning at startup. This setting cannot end in a slash (`/`). -[[server-compression]]`server.compression.enabled:`:: *Default: `true`* Set to `false` to disable HTTP compression for all responses. +| [[server-compression]] `server.compression.enabled:` + | Set to `false` to disable HTTP compression for all responses. *Default: `true`* -`server.compression.referrerWhitelist:`:: *Default: none* Specifies an array of trusted hostnames, such as the Kibana host, or a reverse -proxy sitting in front of it. This determines whether HTTP compression may be used for responses, based on the request's `Referer` header. -This setting may not be used when `server.compression.enabled` is set to `false`. +| `server.compression.referrerWhitelist:` + | Specifies an array of trusted hostnames, such as the {kib} host, or a reverse +proxy sitting in front of it. This determines whether HTTP compression may be used for responses, based on the request `Referer` header. +This setting may not be used when `server.compression.enabled` is set to `false`. *Default: `none`* -`server.customResponseHeaders:`:: *Default: `{}`* Header names and values to - send on all responses to the client from the Kibana server. +| `server.customResponseHeaders:` + | Header names and values to +send on all responses to the client from the {kib} server. *Default: `{}`* -`server.host:`:: *Default: "localhost"* This setting specifies the host of the -back end server. To allow remote users to connect, set the value to the IP address or DNS name of the {kib} server. +| `server.host:` + | This setting specifies the host of the +back end server. To allow remote users to connect, set the value to the IP address or DNS name of the {kib} server. *Default: `"localhost"`* -`server.keepaliveTimeout:`:: *Default: "120000"* The number of milliseconds to wait for additional data before restarting -the `server.socketTimeout` counter. +| `server.keepaliveTimeout:` + | The number of milliseconds to wait for additional data before restarting +the `server.socketTimeout` counter. *Default: `"120000"`* -`server.maxPayloadBytes:`:: *Default: 1048576* The maximum payload size in bytes -for incoming server requests. +| `server.maxPayloadBytes:` + | The maximum payload size in bytes +for incoming server requests. *Default: `1048576`* -`server.name:`:: *Default: "your-hostname"* A human-readable display name that -identifies this Kibana instance. +| `server.name:` + | A human-readable display name that +identifies this {kib} instance. *Default: `"your-hostname"`* -`server.port:`:: *Default: 5601* Kibana is served by a back end server. This -setting specifies the port to use. +| `server.port:` + | {kib} is served by a back end server. This +setting specifies the port to use. *Default: `5601`* -`server.rewriteBasePath:`:: *Default: deprecated* Specifies whether Kibana should +| `server.rewriteBasePath:` + | Specifies whether {kib} should rewrite requests that are prefixed with `server.basePath` or require that they -are rewritten by your reverse proxy. In Kibana 6.3 and earlier, the default is -`false`. In Kibana 7.x, the setting is deprecated. In Kibana 8.0 and later, the -default is `true`. +are rewritten by your reverse proxy. In {kib} 6.3 and earlier, the default is +`false`. In {kib} 7.x, the setting is deprecated. In {kib} 8.0 and later, the +default is `true`. *Default: `deprecated`* -`server.socketTimeout:`:: *Default: "120000"* The number of milliseconds to wait before closing an -inactive socket. +| `server.socketTimeout:` + | The number of milliseconds to wait before closing an +inactive socket. *Default: `"120000"`* -`server.ssl.certificate:` and `server.ssl.key:`:: Paths to a PEM-encoded X.509 server certificate and its corresponding private key. These -are used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. -+ -NOTE: These settings cannot be used in conjunction with `server.ssl.keystore.path`. +| `server.ssl.certificate:` and `server.ssl.key:` + | Paths to a PEM-encoded X.509 server certificate and its corresponding private key. These +are used by {kib} to establish trust when receiving inbound SSL/TLS connections from users. + +|=== -`server.ssl.certificateAuthorities:`:: Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates which make up a +[NOTE] +============ +These settings cannot be used in conjunction with `server.ssl.keystore.path`. +============ + +[cols="2*<"] +|=== + +| `server.ssl.certificateAuthorities:` + | Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates which make up a trusted certificate chain for {kib}. This chain is used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. If PKI authentication is enabled, this chain is also used by {kib} to verify client certificates from end users. + In addition to this setting, trusted certificates may be specified via `server.ssl.keystore.path` and/or `server.ssl.truststore.path`. -`server.ssl.cipherSuites:`:: *Default: ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-GCM-SHA384, DHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, DHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-SHA384, DHE-RSA-AES256-SHA384, ECDHE-RSA-AES256-SHA256, DHE-RSA-AES256-SHA256, HIGH,!aNULL, !eNULL, !EXPORT, !DES, !RC4, !MD5, !PSK, !SRP, !CAMELLIA*. -Details on the format, and the valid options, are available via the +| `server.ssl.cipherSuites:` + | Details on the format, and the valid options, are available via the https://www.openssl.org/docs/man1.0.2/apps/ciphers.html#CIPHER-LIST-FORMAT[OpenSSL cipher list format documentation]. +*Default: `ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-AES256-GCM-SHA384, DHE-RSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-SHA256, DHE-RSA-AES128-SHA256, ECDHE-RSA-AES256-SHA384, DHE-RSA-AES256-SHA384, ECDHE-RSA-AES256-SHA256, DHE-RSA-AES256-SHA256, HIGH,!aNULL, !eNULL, !EXPORT, !DES, !RC4, !MD5, !PSK, !SRP, !CAMELLIA`*. -`server.ssl.clientAuthentication:`:: *Default: `"none"`* Controls {kib}’s behavior in regard to requesting a certificate from client +| `server.ssl.clientAuthentication:` + | Controls the behavior in {kib} for requesting a certificate from client connections. Valid values are `"required"`, `"optional"`, and `"none"`. Using `"required"` will refuse to establish the connection unless a client presents a certificate, using `"optional"` will allow a client to present a certificate if it has one, and using `"none"` will -prevent a client from presenting a certificate. +prevent a client from presenting a certificate. *Default: `"none"`* -`server.ssl.enabled:`:: *Default: `false`* Enables SSL/TLS for inbound connections to {kib}. When set to `true`, a certificate and its +| `server.ssl.enabled:` + | Enables SSL/TLS for inbound connections to {kib}. When set to `true`, a certificate and its corresponding private key must be provided. These can be specified via `server.ssl.keystore.path` or the combination of -`server.ssl.certificate` and `server.ssl.key`. +`server.ssl.certificate` and `server.ssl.key`. *Default: `false`* -`server.ssl.keyPassphrase:`:: The password that will be used to decrypt the private key that is specified via `server.ssl.key`. This value +| `server.ssl.keyPassphrase:` + | The password that decrypts the private key that is specified via `server.ssl.key`. This value is optional, as the key may not be encrypted. -`server.ssl.keystore.path:`:: Path to a PKCS#12 keystore that contains an X.509 server certificate and its corresponding private key. If the +| `server.ssl.keystore.path:` + | Path to a PKCS#12 keystore that contains an X.509 server certificate and its corresponding private key. If the keystore contains any additional certificates, those will be used as a trusted certificate chain for {kib}. All of these are used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. The certificate chain is also used by {kib} to verify client certificates from end users when PKI authentication is enabled. + --- In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or `server.ssl.truststore.path`. -NOTE: This setting cannot be used in conjunction with `server.ssl.certificate` or `server.ssl.key`. --- +|=== + +[NOTE] +============ +This setting cannot be used in conjunction with `server.ssl.certificate` or `server.ssl.key` +============ -`server.ssl.keystore.password:`:: The password that will be used to decrypt the keystore specified via `server.ssl.keystore.path`. If the +[cols="2*<"] +|=== + +| `server.ssl.keystore.password:` + | The password that will be used to decrypt the keystore specified via `server.ssl.keystore.path`. If the keystore has no password, leave this unset. If the keystore has an empty password, set this to `""`. -`server.ssl.truststore.path:`:: Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates which +| `server.ssl.truststore.path:` + | Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates which make up a trusted certificate chain for {kib}. This chain is used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. If PKI authentication is enabled, this chain is also used by {kib} to verify client certificates from end users. + In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or `server.ssl.keystore.path`. -`server.ssl.truststore.password:`:: The password that will be used to decrypt the trust store specified via `server.ssl.truststore.path`. If +| `server.ssl.truststore.password:` + | The password that will be used to decrypt the trust store specified via `server.ssl.truststore.path`. If the trust store has no password, leave this unset. If the trust store has an empty password, set this to `""`. -`server.ssl.redirectHttpFromPort:`:: Kibana will bind to this port and redirect +| `server.ssl.redirectHttpFromPort:` + | {kib} binds to this port and redirects all http requests to https over the port configured as `server.port`. -`server.ssl.supportedProtocols:`:: *Default: TLSv1.1, TLSv1.2* An array of -supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2` +| `server.ssl.supportedProtocols:` + | An array of supported protocols with versions. +Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2`. *Default: TLSv1.1, TLSv1.2* -`server.xsrf.whitelist:`:: It is not recommended to disable protections for +| `server.xsrf.whitelist:` + | It is not recommended to disable protections for arbitrary API endpoints. Instead, supply the `kbn-xsrf` header. The `server.xsrf.whitelist` setting requires the following format: -[source,text] +|=== +[source,text] ---- *Default: [ ]* An array of API endpoints which should be exempt from Cross-Site Request Forgery ("XSRF") protections. ---- -`status.allowAnonymous:`:: *Default: false* If authentication is enabled, -setting this to `true` enables unauthenticated users to access the Kibana -server status API and status page. +[cols="2*<"] +|=== + +| `status.allowAnonymous:` + | If authentication is enabled, +setting this to `true` enables unauthenticated users to access the {kib} +server status API and status page. *Default: `false`* -`telemetry.allowChangingOptInStatus`:: *Default: true*. If `true`, -users are able to change the telemetry setting at a later time in -<>. If `false`, +| `telemetry.allowChangingOptInStatus` + | When `true`, users are able to change the telemetry setting at a later time in +<>. When `false`, {kib} looks at the value of `telemetry.optIn` to determine whether to send telemetry data or not. `telemetry.allowChangingOptInStatus` and `telemetry.optIn` -cannot be `false` at the same time. +cannot be `false` at the same time. *Default: `true`*. -`telemetry.optIn`:: *Default: true* If `true`, telemetry data is sent to Elastic. - If `false`, collection of telemetry data is disabled. - To enable telemetry and prevent users from disabling it, - set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. +| `telemetry.optIn` + | When `true`, telemetry data is sent to Elastic. +When `false`, collection of telemetry data is disabled. +To enable telemetry and prevent users from disabling it, +set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. +*Default: `true`* -`telemetry.enabled`:: *Default: true* Reporting your cluster statistics helps +| `telemetry.enabled` + | Reporting your cluster statistics helps us improve your user experience. Your data is never shared with anyone. Set to `false` to disable telemetry capabilities entirely. You can alternatively opt -out through the *Advanced Settings* in {kib}. +out through *Advanced Settings*. *Default: `true`* + +| `vis_type_vega.enableExternalUrls:` + | Set this value to true to allow Vega to use any URL to access external data +sources and images. When false, Vega can only get data from {es}. *Default: `false`* -`vis_type_vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch. +| `xpack.license_management.enabled` + | Set this value to false to +disable the License Management UI. *Default: `true`* -`xpack.license_management.enabled`:: *Default: true* Set this value to false to -disable the License Management user interface. +| `xpack.rollup.enabled:` + | Set this value to false to disable the +Rollup UI. *Default: true* -`xpack.rollup.enabled:`:: *Default: true* Set this value to false to disable the -Rollup user interface. +| `i18n.locale` + | Set this value to change the {kib} interface language. +Valid locales are: `en`, `zh-CN`, `ja-JP`. *Default: `en`* -`i18n.locale`:: *Default: en* Set this value to change the Kibana interface language. Valid locales are: `en`, `zh-CN`, `ja-JP`. +|=== include::{docdir}/settings/alert-action-settings.asciidoc[] include::{docdir}/settings/apm-settings.asciidoc[] From fb6d325fe92ab9426f6ca4108cad608cff88e592 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 4 May 2020 10:49:14 -0500 Subject: [PATCH 087/122] [DOCS} Fixes 404s in master (#64911) --- docs/redirects.asciidoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index a5503969a3ec1..85d580de9475f 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -70,3 +70,13 @@ This page has moved. Please see <>. == Maps This page has moved. Please see <>. + +[role="exclude",id="development-embedding-visualizations"] +== Embedding Visualizations + +This page was deleted. See <>. + +[role="exclude",id="development-create-visualization"] +== Developing Visualizations + +This page was deleted. See <>. From 86c64af553bec850358c9d86f588489f2d82ac12 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 4 May 2020 18:16:48 +0200 Subject: [PATCH 088/122] Bump mapbox-gl dependency from 1.9.0 to 1.10.0 (#64670) --- x-pack/package.json | 4 +- yarn.lock | 93 +++++++++++++++++++++++++++++---------------- 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index dcc9b8c61cb96..5d1fbaa5784e0 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -81,7 +81,7 @@ "@types/json-stable-stringify": "^1.0.32", "@types/jsonwebtoken": "^7.2.8", "@types/lodash": "^3.10.1", - "@types/mapbox-gl": "^1.8.1", + "@types/mapbox-gl": "^1.9.1", "@types/memoize-one": "^4.1.0", "@types/mime": "^2.0.1", "@types/mocha": "^7.0.2", @@ -280,7 +280,7 @@ "lodash.topath": "^4.5.2", "lodash.uniqby": "^4.7.0", "lz-string": "^1.4.4", - "mapbox-gl": "^1.9.0", + "mapbox-gl": "^1.10.0", "mapbox-gl-draw-rectangle-mode": "^1.0.4", "markdown-it": "^10.0.0", "memoize-one": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 346c4d76d24c9..3c233b76f1a48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2116,7 +2116,7 @@ resolved "https://registry.yarnpkg.com/@mapbox/geojson-normalize/-/geojson-normalize-0.0.1.tgz#1da1e6b3a7add3ad29909b30f438f60581b7cd80" integrity sha1-HaHms6et060pkJsw9Dj2BYG3zYA= -"@mapbox/geojson-rewind@^0.4.0", "@mapbox/geojson-rewind@^0.4.1": +"@mapbox/geojson-rewind@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@mapbox/geojson-rewind/-/geojson-rewind-0.4.1.tgz#357d79300adb7fec7c1f091512988bca6458f068" integrity sha512-mxo2MEr7izA1uOXcDsw99Kgg6xW3P4H2j4n1lmldsgviIelpssvP+jQDivFKOHrOVJDpTTi5oZJvRcHtU9Uufw== @@ -2126,6 +2126,14 @@ minimist "^1.2.5" sharkdown "^0.1.0" +"@mapbox/geojson-rewind@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@mapbox/geojson-rewind/-/geojson-rewind-0.5.0.tgz#91f0ad56008c120caa19414b644d741249f4f560" + integrity sha512-73l/qJQgj/T/zO1JXVfuVvvKDgikD/7D/rHAD28S9BG1OTstgmftrmqfCx4U+zQAmtsB6HcDA3a7ymdnJZAQgg== + dependencies: + concat-stream "~2.0.0" + minimist "^1.2.5" + "@mapbox/geojson-types@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz#9aecf642cb00eab1080a57c4f949a65b4a5846d6" @@ -2166,20 +2174,20 @@ resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-rtl-text/-/mapbox-gl-rtl-text-0.2.3.tgz#a26ecfb3f0061456d93ee8570dd9587d226ea8bd" integrity sha512-RaCYfnxULUUUxNwcUimV9C/o2295ktTyLEUzD/+VWkqXqvaVfFcZ5slytGzb2Sd/Jj4MlbxD0DCZbfa6CzcmMw== -"@mapbox/mapbox-gl-supported@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.4.0.tgz#36946b22944fe2cfa43cfafd5ef36fdb54a069e4" - integrity sha512-ZD0Io4XK+/vU/4zpANjOtdWfVszAgnaMPsGR6LKsWh4kLIEv9qoobTVmJPPuwuM+ZI2b3BlZ6DYw1XHVmv6YTA== +"@mapbox/mapbox-gl-supported@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz#f60b6a55a5d8e5ee908347d2ce4250b15103dc8e" + integrity sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg== "@mapbox/point-geometry@0.1.0", "@mapbox/point-geometry@^0.1.0", "@mapbox/point-geometry@~0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2" integrity sha1-ioP5M1x4YO/6Lu7KJUMyqgru2PI= -"@mapbox/tiny-sdf@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@mapbox/tiny-sdf/-/tiny-sdf-1.1.0.tgz#b0b8f5c22005e6ddb838f421ffd257c1f74f9a20" - integrity sha512-dnhyk8X2BkDRWImgHILYAGgo+kuciNYX30CUKj/Qd5eNjh54OWM/mdOS/PWsPeN+3abtN+QDGYM4G220ynVJKA== +"@mapbox/tiny-sdf@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@mapbox/tiny-sdf/-/tiny-sdf-1.1.1.tgz#16a20c470741bfe9191deb336f46e194da4a91ff" + integrity sha512-Ihn1nZcGIswJ5XGbgFAvVumOgWpvIjBX9jiRlIl46uQG9vJOF51ViBYHF95rEZupuyQbEmhLaDPLQlU7fUTsBg== "@mapbox/unitbezier@^0.0.0": version "0.0.0" @@ -4350,10 +4358,10 @@ resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03" integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w== -"@types/mapbox-gl@^1.8.1": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-1.8.1.tgz#dbc12da1324d5bdb3dbf71b90b77cac17994a1a3" - integrity sha512-DdT/YzpGiYITkj2cUwyqPilPbtZURr1E0vZX0KTyyeNP0t0bxNyKoXo0seAcvUd2MsMgFYwFQh1WRC3x2V0kKQ== +"@types/mapbox-gl@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-1.9.1.tgz#78b62f8a1ead78bc525a4c1db84bb71fa0fcc579" + integrity sha512-5LS/fljbGjCPfjtOK5+pz8TT0PL4bBXTnN/PDbPtTQMqQdY/KWTWE4jRPuo0fL5wctd543DCptEUTydn+JK+gA== dependencies: "@types/geojson" "*" @@ -9539,6 +9547,16 @@ concat-stream@~1.5.0: readable-stream "~2.0.0" typedarray "~0.0.5" +concat-stream@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + conf@^1.1.2, conf@^1.3.1: version "1.4.0" resolved "https://registry.yarnpkg.com/conf/-/conf-1.4.0.tgz#1ea66c9d7a9b601674a5bb9d2b8dc3c726625e67" @@ -10355,7 +10373,7 @@ css@2.X, css@^2.0.0, css@^2.2.1, css@^2.2.3, css@^2.2.4: source-map-resolve "^0.5.2" urix "^0.1.0" -csscolorparser@~1.0.2: +csscolorparser@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" integrity sha1-s085HupNqPPpgjHizNjfnAQfFxs= @@ -14586,10 +14604,10 @@ github-username@^3.0.0: dependencies: gh-got "^5.0.0" -gl-matrix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.0.0.tgz#888301ac7650e148c3865370e13ec66d08a8381f" - integrity sha512-PD4mVH/C/Zs64kOozeFnKY8ybhgwxXXQYGWdB4h68krAHknWJgk9uKOn6z8YElh5//vs++90pb6csrTIDWnexA== +gl-matrix@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.3.0.tgz#232eef60b1c8b30a28cbbe75b2caf6c48fd6358b" + integrity sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA== glob-all@^3.1.0, glob-all@^3.2.1: version "3.2.1" @@ -20091,33 +20109,33 @@ mapbox-gl-draw-rectangle-mode@^1.0.4: resolved "https://registry.yarnpkg.com/mapbox-gl-draw-rectangle-mode/-/mapbox-gl-draw-rectangle-mode-1.0.4.tgz#42987d68872a5fb5cc5d76d3375ee20cd8bab8f7" integrity sha512-BdF6nwEK2p8n9LQoMPzBO8LhddW1fe+d5vK8HQIei+4VcRnUbKNsEj7Z15FsJxCHzsc2BQKXbESx5GaE8x0imQ== -mapbox-gl@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.9.0.tgz#53e3e13c99483f362b07a8a763f2d61d580255a5" - integrity sha512-PKpoiB2pPUMrqFfBJpt/oA8On3zcp0adEoDS2YIC2RA6o4EZ9Sq2NPZocb64y7ra3mLUvEb7ps1pLVlPMh6y7w== +mapbox-gl@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.10.0.tgz#c33e74d1f328e820e245ff8ed7b5dbbbc4be204f" + integrity sha512-SrJXcR9s5yEsPuW2kKKumA1KqYW9RrL8j7ZcIh6glRQ/x3lwNMfwz/UEJAJcVNgeX+fiwzuBoDIdeGB/vSkZLQ== dependencies: - "@mapbox/geojson-rewind" "^0.4.0" + "@mapbox/geojson-rewind" "^0.5.0" "@mapbox/geojson-types" "^1.0.2" "@mapbox/jsonlint-lines-primitives" "^2.0.2" - "@mapbox/mapbox-gl-supported" "^1.4.0" + "@mapbox/mapbox-gl-supported" "^1.5.0" "@mapbox/point-geometry" "^0.1.0" - "@mapbox/tiny-sdf" "^1.1.0" + "@mapbox/tiny-sdf" "^1.1.1" "@mapbox/unitbezier" "^0.0.0" "@mapbox/vector-tile" "^1.3.1" "@mapbox/whoots-js" "^3.1.0" - csscolorparser "~1.0.2" + csscolorparser "~1.0.3" earcut "^2.2.2" geojson-vt "^3.2.1" - gl-matrix "^3.0.0" + gl-matrix "^3.2.1" grid-index "^1.1.0" - minimist "0.0.8" + minimist "^1.2.5" murmurhash-js "^1.0.0" pbf "^3.2.1" potpack "^1.0.1" quickselect "^2.0.0" rw "^1.3.3" supercluster "^7.0.0" - tinyqueue "^2.0.0" + tinyqueue "^2.0.3" vt-pbf "^3.1.1" mapcap@^1.0.0: @@ -25121,6 +25139,15 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.0.2: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readable-stream@~1.1.0: version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" @@ -29031,10 +29058,10 @@ tinymath@1.2.1: resolved "https://registry.yarnpkg.com/tinymath/-/tinymath-1.2.1.tgz#f97ed66c588cdbf3c19dfba2ae266ee323db7e47" integrity sha512-8CYutfuHR3ywAJus/3JUhaJogZap1mrUQGzNxdBiQDhP3H0uFdQenvaXvqI8lMehX4RsanRZzxVfjMBREFdQaA== -tinyqueue@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.2.tgz#b4fe66d28a5b503edb99c149f87910059782a0cc" - integrity sha512-1oUV+ZAQaeaf830ui/p5JZpzGBw46qs1pKHcfqIc6/QxYDQuEmcBLIhiT0xAxLnekz+qxQusubIYk4cAS8TB2A== +tinyqueue@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08" + integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA== title-case@^2.1.0: version "2.1.1" From 496f49247419d80ea3d400a35753ba499f2f2bc3 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 4 May 2020 18:39:09 +0200 Subject: [PATCH 089/122] Fix 37422 (#64215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 rename Filter -> ExpressionValueFilter * refactor: 💡 use new filter type in Canvas * fix: 🐛 fix tests after refactor Co-authored-by: Elastic Machine --- .../common/expression_types/specs/filter.ts | 32 ++++++++++--------- src/plugins/expressions/public/index.ts | 2 +- src/plugins/expressions/server/index.ts | 2 +- .../functions/common/exactly.test.js | 2 +- .../functions/common/exactly.ts | 14 +++++--- .../functions/common/saved_lens.test.ts | 15 +++++++-- .../functions/common/saved_lens.ts | 8 ++--- .../functions/common/saved_map.test.ts | 15 +++++++-- .../functions/common/saved_map.ts | 4 +-- .../functions/common/saved_search.test.ts | 15 +++++++-- .../functions/common/saved_search.ts | 4 +-- .../common/saved_visualization.test.ts | 15 +++++++-- .../functions/common/saved_visualization.ts | 4 +-- .../functions/common/timefilter.test.js | 2 +- .../functions/common/timefilter.ts | 11 ++++--- .../functions/server/demodata.test.ts | 7 ++-- .../functions/server/demodata/index.ts | 9 ++++-- .../functions/server/escount.ts | 15 +++++++-- .../functions/server/esdocs.ts | 12 +++++-- .../functions/server/essql.ts | 9 ++++-- .../canvas/common/lib/datatable/query.js | 4 +-- .../canvas/public/functions/filters.ts | 9 ++++-- .../canvas/public/functions/timelion.ts | 7 ++-- .../lib/build_embeddable_filters.test.ts | 12 ++++--- .../public/lib/build_embeddable_filters.ts | 13 ++++---- .../canvas/server/lib/get_es_filter.js | 8 ++--- x-pack/legacy/plugins/canvas/types/state.ts | 4 +-- 27 files changed, 165 insertions(+), 89 deletions(-) diff --git a/src/plugins/expressions/common/expression_types/specs/filter.ts b/src/plugins/expressions/common/expression_types/specs/filter.ts index 01d6b8a603db6..fc1c086e817c9 100644 --- a/src/plugins/expressions/common/expression_types/specs/filter.ts +++ b/src/plugins/expressions/common/expression_types/specs/filter.ts @@ -17,29 +17,31 @@ * under the License. */ -import { ExpressionTypeDefinition } from '../types'; - -const name = 'filter'; +import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; /** * Represents an object that is a Filter. */ -export interface Filter { - type?: string; - value?: string; - column?: string; - and: Filter[]; - to?: string; - from?: string; - query?: string | null; -} +export type ExpressionValueFilter = ExpressionValueBoxed< + 'filter', + { + filterType?: string; + value?: string; + column?: string; + and: ExpressionValueFilter[]; + to?: string; + from?: string; + query?: string | null; + } +>; -export const filter: ExpressionTypeDefinition = { - name, +export const filter: ExpressionTypeDefinition<'filter', ExpressionValueFilter> = { + name: 'filter', from: { null: () => { return { - type: name, + type: 'filter', + filterType: 'filter', // Any meta data you wish to pass along. meta: {}, // And filters. If you need an "or", create a filter type for it. diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 6814764ee5faa..ee3fbd7a7b0b0 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -78,7 +78,7 @@ export { ExpressionValueRender, ExpressionValueSearchContext, ExpressionValueUnboxed, - Filter, + ExpressionValueFilter, Font, FontLabel, FontStyle, diff --git a/src/plugins/expressions/server/index.ts b/src/plugins/expressions/server/index.ts index e41135b693922..61d3838466bef 100644 --- a/src/plugins/expressions/server/index.ts +++ b/src/plugins/expressions/server/index.ts @@ -69,7 +69,7 @@ export { ExpressionValueRender, ExpressionValueSearchContext, ExpressionValueUnboxed, - Filter, + ExpressionValueFilter, Font, FontLabel, FontStyle, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.test.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.test.js index f03bc54757c3c..2b9bdb59afbdf 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.test.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.test.js @@ -18,7 +18,7 @@ describe('exactly', () => { it("adds an exactly object to 'and'", () => { const result = fn(emptyFilter, { column: 'name', value: 'product2' }); - expect(result.and[0]).toHaveProperty('type', 'exactly'); + expect(result.and[0]).toHaveProperty('filterType', 'exactly'); }); describe('args', () => { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts index 88a24186d6044..5031e8029957b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/exactly.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Filter, ExpressionFunctionDefinition } from '../../../types'; +import { ExpressionValueFilter, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -13,7 +13,12 @@ interface Arguments { filterGroup: string; } -export function exactly(): ExpressionFunctionDefinition<'exactly', Filter, Arguments, Filter> { +export function exactly(): ExpressionFunctionDefinition< + 'exactly', + ExpressionValueFilter, + Arguments, + ExpressionValueFilter +> { const { help, args: argHelp } = getFunctionHelp().exactly; return { @@ -43,8 +48,9 @@ export function exactly(): ExpressionFunctionDefinition<'exactly', Filter, Argum fn: (input, args) => { const { value, column } = args; - const filter = { - type: 'exactly', + const filter: ExpressionValueFilter = { + type: 'filter', + filterType: 'exactly', value, column, and: [], diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts index 6b197148e6373..882d1e2ea58b9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.test.ts @@ -6,14 +6,23 @@ jest.mock('ui/new_platform'); import { savedLens } from './saved_lens'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { ExpressionValueFilter } from '../../../types'; -const filterContext = { +const filterContext: ExpressionValueFilter = { + type: 'filter', and: [ - { and: [], value: 'filter-value', column: 'filter-column', type: 'exactly' }, { + type: 'filter', + and: [], + value: 'filter-value', + column: 'filter-column', + filterType: 'exactly', + }, + { + type: 'filter', and: [], column: 'time-column', - type: 'time', + filterType: 'time', from: '2019-06-04T04:00:00.000Z', to: '2019-06-05T04:00:00.000Z', }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts index 2985a68cf855c..8fc55ddf9cc59 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts @@ -8,7 +8,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { TimeRange, Filter as DataFilter } from 'src/plugins/data/public'; import { EmbeddableInput } from 'src/plugins/embeddable/public'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; -import { Filter, TimeRange as TimeRangeArg } from '../../../types'; +import { ExpressionValueFilter, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, EmbeddableExpressionType, @@ -37,7 +37,7 @@ type Return = EmbeddableExpression; export function savedLens(): ExpressionFunctionDefinition< 'savedLens', - Filter | null, + ExpressionValueFilter | null, Arguments, Return > { @@ -63,8 +63,8 @@ export function savedLens(): ExpressionFunctionDefinition< }, }, type: EmbeddableExpressionType, - fn: (context, args) => { - const filters = context ? context.and : []; + fn: (input, args) => { + const filters = input ? input.and : []; return { type: EmbeddableExpressionType, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts index 63dbae55790a3..74e41a030de35 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.test.ts @@ -6,14 +6,23 @@ jest.mock('ui/new_platform'); import { savedMap } from './saved_map'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { ExpressionValueFilter } from '../../../types'; -const filterContext = { +const filterContext: ExpressionValueFilter = { + type: 'filter', and: [ - { and: [], value: 'filter-value', column: 'filter-column', type: 'exactly' }, { + type: 'filter', + and: [], + value: 'filter-value', + column: 'filter-column', + filterType: 'exactly', + }, + { + type: 'filter', and: [], column: 'time-column', - type: 'time', + filterType: 'time', from: '2019-06-04T04:00:00.000Z', to: '2019-06-05T04:00:00.000Z', }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts index cba19ce7da80f..df316d0dd182f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts @@ -6,7 +6,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; -import { Filter, MapCenter, TimeRange as TimeRangeArg } from '../../../types'; +import { ExpressionValueFilter, MapCenter, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, EmbeddableExpressionType, @@ -32,7 +32,7 @@ type Output = EmbeddableExpression; export function savedMap(): ExpressionFunctionDefinition< 'savedMap', - Filter | null, + ExpressionValueFilter | null, Arguments, Output > { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts index 67356dae5b3e3..9bd32202b563a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.test.ts @@ -6,14 +6,23 @@ jest.mock('ui/new_platform'); import { savedSearch } from './saved_search'; import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; +import { ExpressionValueFilter } from '../../../types'; -const filterContext = { +const filterContext: ExpressionValueFilter = { + type: 'filter', and: [ - { and: [], value: 'filter-value', column: 'filter-column', type: 'exactly' }, { + type: 'filter', + and: [], + value: 'filter-value', + column: 'filter-column', + filterType: 'exactly', + }, + { + type: 'filter', and: [], column: 'time-column', - type: 'time', + filterType: 'time', from: '2019-06-04T04:00:00.000Z', to: '2019-06-05T04:00:00.000Z', }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts index 87dc7eb5e814c..277d035ed0958 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts @@ -13,7 +13,7 @@ import { } from '../../expression_types'; import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; -import { Filter } from '../../../types'; +import { ExpressionValueFilter } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -24,7 +24,7 @@ type Output = EmbeddableExpression & { id: SearchInput['id' export function savedSearch(): ExpressionFunctionDefinition< 'savedSearch', - Filter | null, + ExpressionValueFilter | null, Arguments, Output > { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts index 754a113b87554..8327c1433b9af 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.test.ts @@ -6,14 +6,23 @@ jest.mock('ui/new_platform'); import { savedVisualization } from './saved_visualization'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { ExpressionValueFilter } from '../../../types'; -const filterContext = { +const filterContext: ExpressionValueFilter = { + type: 'filter', and: [ - { and: [], value: 'filter-value', column: 'filter-column', type: 'exactly' }, { + type: 'filter', + and: [], + value: 'filter-value', + column: 'filter-column', + filterType: 'exactly', + }, + { + type: 'filter', and: [], column: 'time-column', - type: 'time', + filterType: 'time', from: '2019-06-04T04:00:00.000Z', to: '2019-06-05T04:00:00.000Z', }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts index d98fea2ec1be8..94c7a1c8a9eea 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts @@ -12,7 +12,7 @@ import { EmbeddableExpression, } from '../../expression_types'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; -import { Filter, TimeRange as TimeRangeArg, SeriesStyle } from '../../../types'; +import { ExpressionValueFilter, TimeRange as TimeRangeArg, SeriesStyle } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -31,7 +31,7 @@ const defaultTimeRange = { export function savedVisualization(): ExpressionFunctionDefinition< 'savedVisualization', - Filter | null, + ExpressionValueFilter | null, Arguments, Output > { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js index aeab0d50c31a7..834b9d195856c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.test.js @@ -44,7 +44,7 @@ describe('timefilter', () => { from: fromDate, to: toDate, }).and[0] - ).toHaveProperty('type', 'time'); + ).toHaveProperty('filterType', 'time'); }); describe('args', () => { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts index 249faf6141b46..ff7b56d8194df 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilter.ts @@ -5,7 +5,7 @@ */ import dateMath from '@elastic/datemath'; -import { Filter, ExpressionFunctionDefinition } from '../../../types'; +import { ExpressionValueFilter, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; interface Arguments { @@ -17,9 +17,9 @@ interface Arguments { export function timefilter(): ExpressionFunctionDefinition< 'timefilter', - Filter, + ExpressionValueFilter, Arguments, - Filter + ExpressionValueFilter > { const { help, args: argHelp } = getFunctionHelp().timefilter; const errors = getFunctionErrors().timefilter; @@ -58,8 +58,9 @@ export function timefilter(): ExpressionFunctionDefinition< } const { from, to, column } = args; - const filter: Filter = { - type: 'time', + const filter: ExpressionValueFilter = { + type: 'filter', + filterType: 'time', column, and: [], }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata.test.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata.test.ts index 94b2d5228665b..2b517664793a7 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata.test.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata.test.ts @@ -5,12 +5,11 @@ */ import { demodata } from './demodata'; +import { ExpressionValueFilter } from '../../../types'; -const nullFilter = { +const nullFilter: ExpressionValueFilter = { type: 'filter', - meta: {}, - size: null, - sort: [], + filterType: 'filter', and: [], }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts index 5cebae5bb669f..843e2bda47e12 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts @@ -10,14 +10,19 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; import { queryDatatable } from '../../../../common/lib/datatable/query'; import { DemoRows } from './demo_rows_types'; import { getDemoRows } from './get_demo_rows'; -import { Filter, Datatable, DatatableColumn, DatatableRow } from '../../../../types'; +import { ExpressionValueFilter, Datatable, DatatableColumn, DatatableRow } from '../../../../types'; import { getFunctionHelp } from '../../../../i18n'; interface Arguments { type: string; } -export function demodata(): ExpressionFunctionDefinition<'demodata', Filter, Arguments, Datatable> { +export function demodata(): ExpressionFunctionDefinition< + 'demodata', + ExpressionValueFilter, + Arguments, + Datatable +> { const { help, args: argHelp } = getFunctionHelp().demodata; return { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/escount.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/escount.ts index ffb8bb4f3e2a7..3f5d0610b4c72 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/escount.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/escount.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionFunctionDefinition, Filter } from 'src/plugins/expressions/common'; +import { + ExpressionFunctionDefinition, + ExpressionValueFilter, +} from 'src/plugins/expressions/common'; // @ts-ignore untyped local import { buildESRequest } from '../../../server/lib/build_es_request'; import { getFunctionHelp } from '../../../i18n'; @@ -14,7 +17,12 @@ interface Arguments { query: string; } -export function escount(): ExpressionFunctionDefinition<'escount', Filter, Arguments, any> { +export function escount(): ExpressionFunctionDefinition< + 'escount', + ExpressionValueFilter, + Arguments, + any +> { const { help, args: argHelp } = getFunctionHelp().escount; return { @@ -40,7 +48,8 @@ export function escount(): ExpressionFunctionDefinition<'escount', Filter, Argum fn: (input, args, handlers) => { input.and = input.and.concat([ { - type: 'luceneQueryString', + type: 'filter', + filterType: 'luceneQueryString', query: args.query, and: [], }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts index 5bff06bb3933b..d60297ee2da3f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts @@ -8,7 +8,7 @@ import squel from 'squel'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; // @ts-ignore untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; -import { Filter } from '../../../types'; +import { ExpressionValueFilter } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -20,7 +20,12 @@ interface Arguments { count: number; } -export function esdocs(): ExpressionFunctionDefinition<'esdocs', Filter, Arguments, any> { +export function esdocs(): ExpressionFunctionDefinition< + 'esdocs', + ExpressionValueFilter, + Arguments, + any +> { const { help, args: argHelp } = getFunctionHelp().esdocs; return { @@ -67,7 +72,8 @@ export function esdocs(): ExpressionFunctionDefinition<'esdocs', Filter, Argumen input.and = input.and.concat([ { - type: 'luceneQueryString', + type: 'filter', + filterType: 'luceneQueryString', query: args.query, and: [], }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/essql.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/essql.ts index cdb6b5af82015..b972f5a3bd4a6 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/essql.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/essql.ts @@ -7,7 +7,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; // @ts-ignore untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; -import { Filter } from '../../../types'; +import { ExpressionValueFilter } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -16,7 +16,12 @@ interface Arguments { timezone: string; } -export function essql(): ExpressionFunctionDefinition<'essql', Filter, Arguments, any> { +export function essql(): ExpressionFunctionDefinition< + 'essql', + ExpressionValueFilter, + Arguments, + any +> { const { help, args: argHelp } = getFunctionHelp().essql; return { diff --git a/x-pack/legacy/plugins/canvas/common/lib/datatable/query.js b/x-pack/legacy/plugins/canvas/common/lib/datatable/query.js index f61e2b6434697..63945ce7690f9 100644 --- a/x-pack/legacy/plugins/canvas/common/lib/datatable/query.js +++ b/x-pack/legacy/plugins/canvas/common/lib/datatable/query.js @@ -15,14 +15,14 @@ export function queryDatatable(datatable, query) { if (query.and) { query.and.forEach(filter => { // handle exact matches - if (filter.type === 'exactly') { + if (filter.filterType === 'exactly') { datatable.rows = datatable.rows.filter(row => { return row[filter.column] === filter.value; }); } // handle time filters - if (filter.type === 'time') { + if (filter.filterType === 'time') { const columnNames = datatable.columns.map(col => col.name); // remove row if no column match diff --git a/x-pack/legacy/plugins/canvas/public/functions/filters.ts b/x-pack/legacy/plugins/canvas/public/functions/filters.ts index 2a3bc481d7dae..16d0bb0fff708 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/filters.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/filters.ts @@ -11,7 +11,7 @@ import { interpretAst } from '../lib/run_interpreter'; // @ts-ignore untyped local import { getState } from '../state/store'; import { getGlobalFilters } from '../state/selectors/workpad'; -import { Filter } from '../../types'; +import { ExpressionValueFilter } from '../../types'; import { getFunctionHelp } from '../../i18n'; import { InitializeArguments } from '.'; @@ -41,7 +41,12 @@ function getFiltersByGroup(allFilters: string[], groups?: string[], ungrouped = }); } -type FiltersFunction = ExpressionFunctionDefinition<'filters', null, Arguments, Filter>; +type FiltersFunction = ExpressionFunctionDefinition< + 'filters', + null, + Arguments, + ExpressionValueFilter +>; export function filtersFunctionFactory(initialize: InitializeArguments): () => FiltersFunction { return function filters(): FiltersFunction { diff --git a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts index e59d798108945..d07b3bf6d1d1c 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts @@ -11,7 +11,7 @@ import { ExpressionFunctionDefinition, DatatableRow } from 'src/plugins/expressi import { fetch } from '../../common/lib/fetch'; // @ts-ignore untyped local import { buildBoolArray } from '../../server/lib/build_bool_array'; -import { Datatable, Filter } from '../../types'; +import { Datatable, ExpressionValueFilter } from '../../types'; import { getFunctionHelp } from '../../i18n'; import { InitializeArguments } from './'; @@ -49,7 +49,7 @@ function parseDateMath( type TimelionFunction = ExpressionFunctionDefinition< 'timelion', - Filter, + ExpressionValueFilter, Arguments, Promise >; @@ -94,11 +94,10 @@ export function timelionFunctionFactory(initialize: InitializeArguments): () => fn: (input, args): Promise => { // Timelion requires a time range. Use the time range from the timefilter element in the // workpad, if it exists. Otherwise fall back on the function args. - const timeFilter = input.and.find(and => and.type === 'time'); + const timeFilter = input.and.find(and => and.filterType === 'time'); const range = timeFilter ? { min: timeFilter.from, max: timeFilter.to } : parseDateMath({ from: args.from, to: args.to }, args.timezone, initialize.timefilter); - const body = { extended: { es: { diff --git a/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.test.ts b/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.test.ts index b422a9451293f..77be181d47378 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.test.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.test.ts @@ -5,19 +5,21 @@ */ import { buildEmbeddableFilters } from './build_embeddable_filters'; -import { Filter } from '../../types'; +import { ExpressionValueFilter } from '../../types'; -const columnFilter: Filter = { +const columnFilter: ExpressionValueFilter = { + type: 'filter', and: [], value: 'filter-value', column: 'filter-column', - type: 'exactly', + filterType: 'exactly', }; -const timeFilter: Filter = { +const timeFilter: ExpressionValueFilter = { + type: 'filter', and: [], column: 'time-column', - type: 'time', + filterType: 'time', from: '2019-06-04T04:00:00.000Z', to: '2019-06-05T04:00:00.000Z', }; diff --git a/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.ts b/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.ts index 1a5d2119a94b6..aa915d0d3d02a 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/build_embeddable_filters.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Filter } from '../../types'; +import { ExpressionValueFilter } from '../../types'; // @ts-ignore Untyped Local import { buildBoolArray } from './build_bool_array'; import { @@ -20,9 +20,9 @@ export interface EmbeddableFilterInput { const TimeFilterType = 'time'; -function getTimeRangeFromFilters(filters: Filter[]): TimeRange | undefined { +function getTimeRangeFromFilters(filters: ExpressionValueFilter[]): TimeRange | undefined { const timeFilter = filters.find( - filter => filter.type !== undefined && filter.type === TimeFilterType + filter => filter.filterType !== undefined && filter.filterType === TimeFilterType ); return timeFilter !== undefined && timeFilter.from !== undefined && timeFilter.to !== undefined @@ -33,11 +33,12 @@ function getTimeRangeFromFilters(filters: Filter[]): TimeRange | undefined { : undefined; } -export function getQueryFilters(filters: Filter[]): DataFilter[] { - return buildBoolArray(filters).map(esFilters.buildQueryFilter); +export function getQueryFilters(filters: ExpressionValueFilter[]): DataFilter[] { + const dataFilters = filters.map(filter => ({ ...filter, type: filter.filterType })); + return buildBoolArray(dataFilters).map(esFilters.buildQueryFilter); } -export function buildEmbeddableFilters(filters: Filter[]): EmbeddableFilterInput { +export function buildEmbeddableFilters(filters: ExpressionValueFilter[]): EmbeddableFilterInput { return { timeRange: getTimeRangeFromFilters(filters), filters: getQueryFilters(filters), diff --git a/x-pack/legacy/plugins/canvas/server/lib/get_es_filter.js b/x-pack/legacy/plugins/canvas/server/lib/get_es_filter.js index e8a4d704118e8..7c025ed8dee9b 100644 --- a/x-pack/legacy/plugins/canvas/server/lib/get_es_filter.js +++ b/x-pack/legacy/plugins/canvas/server/lib/get_es_filter.js @@ -14,13 +14,13 @@ import * as filters from './filters'; export function getESFilter(filter) { - if (!filters[filter.type]) { - throw new Error(`Unknown filter type: ${filter.type}`); + if (!filters[filter.filterType]) { + throw new Error(`Unknown filter type: ${filter.filterType}`); } try { - return filters[filter.type](filter); + return filters[filter.filterType](filter); } catch (e) { - throw new Error(`Could not create elasticsearch filter from ${filter.type}`); + throw new Error(`Could not create elasticsearch filter from ${filter.filterType}`); } } diff --git a/x-pack/legacy/plugins/canvas/types/state.ts b/x-pack/legacy/plugins/canvas/types/state.ts index 13c8f7a9176ab..e9b580f81e668 100644 --- a/x-pack/legacy/plugins/canvas/types/state.ts +++ b/x-pack/legacy/plugins/canvas/types/state.ts @@ -6,7 +6,7 @@ import { Datatable, - Filter, + ExpressionValueFilter, ExpressionImage, ExpressionFunction, KibanaContext, @@ -46,7 +46,7 @@ interface ElementStatsType { type ExpressionType = | Datatable - | Filter + | ExpressionValueFilter | ExpressionImage | KibanaContext | KibanaDatatable From 306a5fe55ebbf05d095e5f0504e495cef2032049 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 4 May 2020 10:53:06 -0600 Subject: [PATCH 090/122] Use brotli compression for some KP assets (#64367) --- package.json | 2 + packages/kbn-optimizer/package.json | 2 + .../basic_optimization.test.ts | 39 +++++++--- .../src/worker/webpack.config.ts | 11 +++ packages/kbn-ui-shared-deps/package.json | 1 + packages/kbn-ui-shared-deps/webpack.config.js | 11 +++ renovate.json5 | 2 + src/dev/renovate/package_groups.ts | 12 ++- .../bundles_route/dynamic_asset_response.ts | 45 +++++++++++- test/functional/apps/bundles/index.js | 73 +++++++++++++++++++ test/functional/config.js | 3 +- test/functional/services/index.ts | 2 + test/functional/services/supertest.ts | 29 ++++++++ typings/accept.d.ts | 23 ++++++ yarn.lock | 42 ++++++++--- 15 files changed, 274 insertions(+), 23 deletions(-) create mode 100644 test/functional/apps/bundles/index.js create mode 100644 test/functional/services/supertest.ts create mode 100644 typings/accept.d.ts diff --git a/package.json b/package.json index 1e3ddc976aa67..e711235e16ea5 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "@types/tar": "^4.0.3", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", + "accept": "3.0.2", "angular": "^1.7.9", "angular-aria": "^1.7.9", "angular-elastic": "^2.5.1", @@ -310,6 +311,7 @@ "@percy/agent": "^0.26.0", "@testing-library/react": "^9.3.2", "@testing-library/react-hooks": "^3.2.1", + "@types/accept": "3.1.1", "@types/angular": "^1.6.56", "@types/angular-mocks": "^1.7.0", "@types/babel__core": "^7.1.2", diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index b3e5a8c518682..b7c9a63897bf9 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -14,6 +14,7 @@ "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", + "@types/compression-webpack-plugin": "^2.0.1", "@types/estree": "^0.0.44", "@types/loader-utils": "^1.1.3", "@types/watchpack": "^1.1.5", @@ -23,6 +24,7 @@ "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "clean-webpack-plugin": "^3.0.0", + "compression-webpack-plugin": "^3.1.0", "cpy": "^8.0.0", "css-loader": "^3.4.2", "del": "^5.1.0", diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index ad743933e1171..248b0b7cf4c97 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -19,6 +19,7 @@ import Path from 'path'; import Fs from 'fs'; +import Zlib from 'zlib'; import { inspect } from 'util'; import cpy from 'cpy'; @@ -124,17 +125,12 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { ); assert('produce zero unexpected states', otherStates.length === 0, otherStates); - expect( - Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/foo.plugin.js'), 'utf8') - ).toMatchSnapshot('foo bundle'); - - expect( - Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/foo/target/public/1.plugin.js'), 'utf8') - ).toMatchSnapshot('1 async bundle'); - - expect( - Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, 'plugins/bar/target/public/bar.plugin.js'), 'utf8') - ).toMatchSnapshot('bar bundle'); + expectFileMatchesSnapshotWithCompression('plugins/foo/target/public/foo.plugin.js', 'foo bundle'); + expectFileMatchesSnapshotWithCompression( + 'plugins/foo/target/public/1.plugin.js', + '1 async bundle' + ); + expectFileMatchesSnapshotWithCompression('plugins/bar/target/public/bar.plugin.js', 'bar bundle'); const foo = config.bundles.find(b => b.id === 'foo')!; expect(foo).toBeTruthy(); @@ -203,3 +199,24 @@ it('uses cache on second run and exist cleanly', async () => { ] `); }); + +/** + * Verifies that the file matches the expected output and has matching compressed variants. + */ +const expectFileMatchesSnapshotWithCompression = (filePath: string, snapshotLabel: string) => { + const raw = Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, filePath), 'utf8'); + expect(raw).toMatchSnapshot(snapshotLabel); + + // Verify the brotli variant matches + expect( + // @ts-ignore @types/node is missing the brotli functions + Zlib.brotliDecompressSync( + Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, `${filePath}.br`)) + ).toString() + ).toEqual(raw); + + // Verify the gzip variant matches + expect( + Zlib.gunzipSync(Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, `${filePath}.gz`))).toString() + ).toEqual(raw); +}; diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index cc3fa8c2720de..95e826e7620aa 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -28,6 +28,7 @@ import TerserPlugin from 'terser-webpack-plugin'; import webpackMerge from 'webpack-merge'; // @ts-ignore import { CleanWebpackPlugin } from 'clean-webpack-plugin'; +import CompressionPlugin from 'compression-webpack-plugin'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { Bundle, WorkerConfig, parseDirPath, DisallowedSyntaxPlugin } from '../common'; @@ -319,6 +320,16 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { IS_KIBANA_DISTRIBUTABLE: `"true"`, }, }), + new CompressionPlugin({ + algorithm: 'brotliCompress', + filename: '[path].br', + test: /\.(js|css)$/, + }), + new CompressionPlugin({ + algorithm: 'gzip', + filename: '[path].gz', + test: /\.(js|css)$/, + }), ], optimization: { diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index a60e2b0449d95..ec61e8519c960 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -14,6 +14,7 @@ "@kbn/i18n": "1.0.0", "abortcontroller-polyfill": "^1.4.0", "angular": "^1.7.9", + "compression-webpack-plugin": "^3.1.0", "core-js": "^3.6.4", "custom-event-polyfill": "^0.3.0", "elasticsearch-browser": "^16.7.0", diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index bf63c57765859..52e7bb620b50b 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -20,6 +20,7 @@ const Path = require('path'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const CompressionPlugin = require('compression-webpack-plugin'); const { REPO_ROOT } = require('@kbn/dev-utils'); const webpack = require('webpack'); @@ -117,5 +118,15 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ new webpack.DefinePlugin({ 'process.env.NODE_ENV': dev ? '"development"' : '"production"', }), + new CompressionPlugin({ + algorithm: 'brotliCompress', + filename: '[path].br', + test: /\.(js|css)$/, + }), + new CompressionPlugin({ + algorithm: 'gzip', + filename: '[path].gz', + test: /\.(js|css)$/, + }), ], }); diff --git a/renovate.json5 b/renovate.json5 index c0ddcaf4f23c8..61b2485ecf44b 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -398,6 +398,8 @@ '@types/good-squeeze', 'inert', '@types/inert', + 'accept', + '@types/accept', ], }, { diff --git a/src/dev/renovate/package_groups.ts b/src/dev/renovate/package_groups.ts index 1bc65fd149f47..9f5aa8556ac21 100644 --- a/src/dev/renovate/package_groups.ts +++ b/src/dev/renovate/package_groups.ts @@ -159,7 +159,17 @@ export const RENOVATE_PACKAGE_GROUPS: PackageGroup[] = [ { name: 'hapi', packageWords: ['hapi'], - packageNames: ['hapi', 'joi', 'boom', 'hoek', 'h2o2', '@elastic/good', 'good-squeeze', 'inert'], + packageNames: [ + 'hapi', + 'joi', + 'boom', + 'hoek', + 'h2o2', + '@elastic/good', + 'good-squeeze', + 'inert', + 'accept', + ], }, { diff --git a/src/optimize/bundles_route/dynamic_asset_response.ts b/src/optimize/bundles_route/dynamic_asset_response.ts index a020c6935eeec..2f5395341abb1 100644 --- a/src/optimize/bundles_route/dynamic_asset_response.ts +++ b/src/optimize/bundles_route/dynamic_asset_response.ts @@ -21,6 +21,7 @@ import Fs from 'fs'; import { resolve } from 'path'; import { promisify } from 'util'; +import Accept from 'accept'; import Boom from 'boom'; import Hapi from 'hapi'; @@ -37,6 +38,41 @@ const asyncOpen = promisify(Fs.open); const asyncClose = promisify(Fs.close); const asyncFstat = promisify(Fs.fstat); +async function tryToOpenFile(filePath: string) { + try { + return await asyncOpen(filePath, 'r'); + } catch (e) { + if (e.code === 'ENOENT') { + return undefined; + } else { + throw e; + } + } +} + +async function selectCompressedFile(acceptEncodingHeader: string | undefined, path: string) { + let fd: number | undefined; + let fileEncoding: 'gzip' | 'br' | undefined; + + const supportedEncodings = Accept.encodings(acceptEncodingHeader, ['br', 'gzip']); + + if (supportedEncodings[0] === 'br') { + fileEncoding = 'br'; + fd = await tryToOpenFile(`${path}.br`); + } + if (!fd && supportedEncodings.includes('gzip')) { + fileEncoding = 'gzip'; + fd = await tryToOpenFile(`${path}.gz`); + } + if (!fd) { + fileEncoding = undefined; + // Use raw open to trigger exception if it does not exist + fd = await asyncOpen(path, 'r'); + } + + return { fd, fileEncoding }; +} + /** * Create a Hapi response for the requested path. This is designed * to replicate a subset of the features provided by Hapi's Inert @@ -74,6 +110,7 @@ export async function createDynamicAssetResponse({ isDist: boolean; }) { let fd: number | undefined; + let fileEncoding: 'gzip' | 'br' | undefined; try { const path = resolve(bundlesPath, request.params.path); @@ -86,7 +123,7 @@ export async function createDynamicAssetResponse({ // we use and manage a file descriptor mostly because // that's what Inert does, and since we are accessing // the file 2 or 3 times per request it seems logical - fd = await asyncOpen(path, 'r'); + ({ fd, fileEncoding } = await selectCompressedFile(request.headers['accept-encoding'], path)); const stat = await asyncFstat(fd); const hash = isDist ? undefined : await getFileHash(fileHashCache, path, stat, fd); @@ -113,6 +150,12 @@ export async function createDynamicAssetResponse({ response.header('cache-control', 'must-revalidate'); } + // If we manually selected a compressed file, specify the encoding header. + // Otherwise, let Hapi automatically gzip the response. + if (fileEncoding) { + response.header('content-encoding', fileEncoding); + } + return response; } catch (error) { if (fd) { diff --git a/test/functional/apps/bundles/index.js b/test/functional/apps/bundles/index.js new file mode 100644 index 0000000000000..8a25c7cd1fafc --- /dev/null +++ b/test/functional/apps/bundles/index.js @@ -0,0 +1,73 @@ +/* + * 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. + */ + +/** + * These supertest-based tests live in the functional test suite because they depend on the optimizer bundles being built + * and served + */ +export default function({ getService }) { + const supertest = getService('supertest'); + + describe('bundle compression', function() { + this.tags('ciGroup12'); + + let buildNum; + before(async () => { + const resp = await supertest.get('/api/status').expect(200); + buildNum = resp.body.version.build_number; + }); + + it('returns gzip files when client only supports gzip', () => + supertest + // We use the kbn-ui-shared-deps for these tests since they are always built with br compressed outputs, + // even in dev. Bundles built by @kbn/optimizer are only built with br compression in dist mode. + .get(`/${buildNum}/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.js`) + .set('Accept-Encoding', 'gzip') + .expect(200) + .expect('Content-Encoding', 'gzip')); + + it('returns br files when client only supports br', () => + supertest + .get(`/${buildNum}/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.js`) + .set('Accept-Encoding', 'br') + .expect(200) + .expect('Content-Encoding', 'br')); + + it('returns br files when client only supports gzip and br', () => + supertest + .get(`/${buildNum}/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.js`) + .set('Accept-Encoding', 'gzip, br') + .expect(200) + .expect('Content-Encoding', 'br')); + + it('returns gzip files when client prefers gzip', () => + supertest + .get(`/${buildNum}/bundles/kbn-ui-shared-deps/kbn-ui-shared-deps.js`) + .set('Accept-Encoding', 'gzip;q=1.0, br;q=0.5') + .expect(200) + .expect('Content-Encoding', 'gzip')); + + it('returns gzip files when no brotli version exists', () => + supertest + .get(`/${buildNum}/bundles/commons.style.css`) // legacy optimizer does not create brotli outputs + .set('Accept-Encoding', 'gzip, br') + .expect(200) + .expect('Content-Encoding', 'gzip')); + }); +} diff --git a/test/functional/config.js b/test/functional/config.js index 0fbde95afe12c..8cc0a34e352a9 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -25,11 +25,12 @@ export default async function({ readConfigFile }) { return { testFiles: [ + require.resolve('./apps/bundles'), require.resolve('./apps/console'), - require.resolve('./apps/getting_started'), require.resolve('./apps/context'), require.resolve('./apps/dashboard'), require.resolve('./apps/discover'), + require.resolve('./apps/getting_started'), require.resolve('./apps/home'), require.resolve('./apps/management'), require.resolve('./apps/saved_objects_management'), diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index a10bb013b3af4..02ed9e9865d9a 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -51,6 +51,7 @@ import { ToastsProvider } from './toasts'; import { PieChartProvider } from './visualizations'; import { ListingTableProvider } from './listing_table'; import { SavedQueryManagementComponentProvider } from './saved_query_management_component'; +import { KibanaSupertestProvider } from './supertest'; export const services = { ...commonServiceProviders, @@ -83,4 +84,5 @@ export const services = { toasts: ToastsProvider, savedQueryManagementComponent: SavedQueryManagementComponentProvider, elasticChart: ElasticChartProvider, + supertest: KibanaSupertestProvider, }; diff --git a/test/functional/services/supertest.ts b/test/functional/services/supertest.ts new file mode 100644 index 0000000000000..30c7db87a8f8b --- /dev/null +++ b/test/functional/services/supertest.ts @@ -0,0 +1,29 @@ +/* + * 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 { FtrProviderContext } from 'test/functional/ftr_provider_context'; +import { format as formatUrl } from 'url'; + +import supertestAsPromised from 'supertest-as-promised'; + +export function KibanaSupertestProvider({ getService }: FtrProviderContext) { + const config = getService('config'); + const kibanaServerUrl = formatUrl(config.get('servers.kibana')); + return supertestAsPromised(kibanaServerUrl); +} diff --git a/typings/accept.d.ts b/typings/accept.d.ts new file mode 100644 index 0000000000000..69cadc7491eeb --- /dev/null +++ b/typings/accept.d.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +declare module 'accept' { + // @types/accept does not include the `preferences` argument so we override the type to include it + export function encodings(encodingHeader?: string, preferences?: string[]): string[]; +} diff --git a/yarn.lock b/yarn.lock index 3c233b76f1a48..941143e76483e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3624,6 +3624,11 @@ dependencies: "@turf/helpers" "6.x" +"@types/accept@3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/accept/-/accept-3.1.1.tgz#74457f6afabd23181e32b6bafae238bda0ce0da7" + integrity sha512-pXwi0bKUriKuNUv7d1xwbxKTqyTIzmMr1StxcGARmiuTLQyjNo+YwDq0w8dzY8wQjPofdgs1hvQLTuJaGuSKiQ== + "@types/angular-mocks@^1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@types/angular-mocks/-/angular-mocks-1.7.0.tgz#310d999a3c47c10ecd8eef466b5861df84799429" @@ -3852,6 +3857,13 @@ dependencies: "@types/color-convert" "*" +"@types/compression-webpack-plugin@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/compression-webpack-plugin/-/compression-webpack-plugin-2.0.1.tgz#4db78c398c8e973077cc530014d6513f1c693951" + integrity sha512-40oKg2aByfUPShpYBkldYwOcO34yaqOIPdlUlR1+F3MFl2WfpqYq2LFKOcgjU70d1r1L8r99XHkxYdhkGajHSw== + dependencies: + "@types/webpack" "*" + "@types/cookiejar@*": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.0.tgz#4b7daf2c51696cfc70b942c11690528229d1a1ce" @@ -5454,7 +5466,7 @@ abortcontroller-polyfill@^1.4.0: resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.4.0.tgz#0d5eb58e522a461774af8086414f68e1dda7a6c4" integrity sha512-3ZFfCRfDzx3GFjO6RAkYx81lPGpUS20ISxux9gLxuKnqafNcFQo59+IoZqpO2WvQlyc287B62HDnDdNYRmlvWA== -accept@3.x.x: +accept@3.0.2, accept@3.x.x: version "3.0.2" resolved "https://registry.yarnpkg.com/accept/-/accept-3.0.2.tgz#83e41cec7e1149f3fd474880423873db6c6cc9ac" integrity sha512-bghLXFkCOsC1Y2TZ51etWfKDs6q249SAoHTZVfzWWdlZxoij+mgkj9AmUJWQpDY48TfnrTDIe43Xem4zdMe7mQ== @@ -9501,6 +9513,18 @@ compressible@~2.0.16: dependencies: mime-db ">= 1.40.0 < 2" +compression-webpack-plugin@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-3.1.0.tgz#9f510172a7b5fae5aad3b670652e8bd7997aeeca" + integrity sha512-iqTHj3rADN4yHwXMBrQa/xrncex/uEQy8QHlaTKxGchT/hC0SdlJlmL/5eRqffmWq2ep0/Romw6Ld39JjTR/ug== + dependencies: + cacache "^13.0.1" + find-cache-dir "^3.0.0" + neo-async "^2.5.0" + schema-utils "^2.6.1" + serialize-javascript "^2.1.2" + webpack-sources "^1.0.1" + compression@^1.7.4: version "1.7.4" resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" @@ -31654,18 +31678,18 @@ webpack-merge@4.2.2, webpack-merge@^4.2.2: dependencies: lodash "^4.17.15" -webpack-sources@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" - integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA== +webpack-sources@^1.0.1, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== dependencies: source-list-map "^2.0.0" source-map "~0.6.1" -webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== +webpack-sources@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" + integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA== dependencies: source-list-map "^2.0.0" source-map "~0.6.1" From 6ab1b20eeb5587d66535ff9c97f54370b67ea0a1 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 4 May 2020 10:53:47 -0600 Subject: [PATCH 091/122] Display global loading bar while applications are mounting (#64556) --- .../application_service.test.ts.snap | 1 + .../application/application_service.test.ts | 77 +++++++++++++++- .../application/application_service.tsx | 4 + .../integration_tests/router.test.tsx | 30 ++++--- .../application/integration_tests/utils.tsx | 5 +- .../public/application/ui/app_container.scss | 25 ++++++ .../application/ui/app_container.test.tsx | 90 ++++++++++++++++++- .../public/application/ui/app_container.tsx | 35 ++++++-- src/core/public/application/ui/app_router.tsx | 6 +- 9 files changed, 242 insertions(+), 31 deletions(-) create mode 100644 src/core/public/application/ui/app_container.scss diff --git a/src/core/public/application/__snapshots__/application_service.test.ts.snap b/src/core/public/application/__snapshots__/application_service.test.ts.snap index 376b320b64ea9..c085fb028cd5a 100644 --- a/src/core/public/application/__snapshots__/application_service.test.ts.snap +++ b/src/core/public/application/__snapshots__/application_service.test.ts.snap @@ -80,5 +80,6 @@ exports[`#start() getComponent returns renderable JSX tree 1`] = ` } mounters={Map {}} setAppLeaveHandler={[Function]} + setIsMounting={[Function]} /> `; diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index e29837aecb125..04ff844ffc150 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -20,7 +20,7 @@ import { createElement } from 'react'; import { BehaviorSubject, Subject } from 'rxjs'; import { bufferCount, take, takeUntil } from 'rxjs/operators'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; @@ -30,6 +30,7 @@ import { MockCapabilitiesService, MockHistory } from './application_service.test import { MockLifecycle } from './test_types'; import { ApplicationService } from './application_service'; import { App, AppNavLinkStatus, AppStatus, AppUpdater, LegacyApp } from './types'; +import { act } from 'react-dom/test-utils'; const createApp = (props: Partial): App => { return { @@ -452,9 +453,9 @@ describe('#setup()', () => { const container = setupDeps.context.createContextContainer.mock.results[0].value; const pluginId = Symbol(); - const mount = () => () => undefined; - registerMountContext(pluginId, 'test' as any, mount); - expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', mount); + const appMount = () => () => undefined; + registerMountContext(pluginId, 'test' as any, appMount); + expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', appMount); }); }); @@ -809,6 +810,74 @@ describe('#start()', () => { `); }); + it('updates httpLoadingCount$ while mounting', async () => { + // Use a memory history so that mounting the component will work + const { createMemoryHistory } = jest.requireActual('history'); + const history = createMemoryHistory(); + setupDeps.history = history; + + const flushPromises = () => new Promise(resolve => setImmediate(resolve)); + // Create an app and a promise that allows us to control when the app completes mounting + const createWaitingApp = (props: Partial): [App, () => void] => { + let finishMount: () => void; + const mountPromise = new Promise(resolve => (finishMount = resolve)); + const app = { + id: 'some-id', + title: 'some-title', + mount: async () => { + await mountPromise; + return () => undefined; + }, + ...props, + }; + + return [app, finishMount!]; + }; + + // Create some dummy applications + const { register } = service.setup(setupDeps); + const [alphaApp, finishAlphaMount] = createWaitingApp({ id: 'alpha' }); + const [betaApp, finishBetaMount] = createWaitingApp({ id: 'beta' }); + register(Symbol(), alphaApp); + register(Symbol(), betaApp); + + const { navigateToApp, getComponent } = await service.start(startDeps); + const httpLoadingCount$ = startDeps.http.addLoadingCountSource.mock.calls[0][0]; + const stop$ = new Subject(); + const currentLoadingCount$ = new BehaviorSubject(0); + httpLoadingCount$.pipe(takeUntil(stop$)).subscribe(currentLoadingCount$); + const loadingPromise = httpLoadingCount$.pipe(bufferCount(5), takeUntil(stop$)).toPromise(); + mount(getComponent()!); + + await act(() => navigateToApp('alpha')); + expect(currentLoadingCount$.value).toEqual(1); + await act(async () => { + finishAlphaMount(); + await flushPromises(); + }); + expect(currentLoadingCount$.value).toEqual(0); + + await act(() => navigateToApp('beta')); + expect(currentLoadingCount$.value).toEqual(1); + await act(async () => { + finishBetaMount(); + await flushPromises(); + }); + expect(currentLoadingCount$.value).toEqual(0); + + stop$.next(); + const loadingCounts = await loadingPromise; + expect(loadingCounts).toMatchInlineSnapshot(` + Array [ + 0, + 1, + 0, + 1, + 0, + ] + `); + }); + it('sets window.location.href when navigating to legacy apps', async () => { setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' }); setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index bafa1932e5e92..0dd77072e9eaf 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -238,6 +238,9 @@ export class ApplicationService { throw new Error('ApplicationService#setup() must be invoked before start.'); } + const httpLoadingCount$ = new BehaviorSubject(0); + http.addLoadingCountSource(httpLoadingCount$); + this.registrationClosed = true; window.addEventListener('beforeunload', this.onBeforeUnload); @@ -303,6 +306,7 @@ export class ApplicationService { mounters={availableMounters} appStatuses$={applicationStatuses$} setAppLeaveHandler={this.setAppLeaveHandler} + setIsMounting={isMounting => httpLoadingCount$.next(isMounting ? 1 : 0)} /> ); }, diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index 2f26bc1409104..915c58b28ad6d 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -40,7 +40,7 @@ describe('AppContainer', () => { }; const mockMountersToMounters = () => new Map([...mounters].map(([appId, { mounter }]) => [appId, mounter])); - const setAppLeaveHandlerMock = () => undefined; + const noop = () => undefined; const mountersToAppStatus$ = () => { return new BehaviorSubject( @@ -86,7 +86,8 @@ describe('AppContainer', () => { history={globalHistory} mounters={mockMountersToMounters()} appStatuses$={appStatuses$} - setAppLeaveHandler={setAppLeaveHandlerMock} + setAppLeaveHandler={noop} + setIsMounting={noop} /> ); }); @@ -98,7 +99,7 @@ describe('AppContainer', () => { expect(app1.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
    + "
    basename: /app/app1 html: App 1
    " @@ -110,7 +111,7 @@ describe('AppContainer', () => { expect(app1Unmount).toHaveBeenCalled(); expect(app2.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
    + "
    basename: /app/app2 html:
    App 2
    " @@ -124,7 +125,7 @@ describe('AppContainer', () => { expect(standardApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
    + "
    basename: /app/app1 html: App 1
    " @@ -136,7 +137,7 @@ describe('AppContainer', () => { expect(standardAppUnmount).toHaveBeenCalled(); expect(chromelessApp.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
    + "
    basename: /chromeless-a/path html:
    Chromeless A
    " @@ -148,7 +149,7 @@ describe('AppContainer', () => { expect(chromelessAppUnmount).toHaveBeenCalled(); expect(standardApp.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
    + "
    basename: /app/app1 html: App 1
    " @@ -162,7 +163,7 @@ describe('AppContainer', () => { expect(chromelessAppA.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
    + "
    basename: /chromeless-a/path html:
    Chromeless A
    " @@ -174,7 +175,7 @@ describe('AppContainer', () => { expect(chromelessAppAUnmount).toHaveBeenCalled(); expect(chromelessAppB.mounter.mount).toHaveBeenCalled(); expect(dom?.html()).toMatchInlineSnapshot(` - "
    + "
    basename: /chromeless-b/path html:
    Chromeless B
    " @@ -186,7 +187,7 @@ describe('AppContainer', () => { expect(chromelessAppBUnmount).toHaveBeenCalled(); expect(chromelessAppA.mounter.mount).toHaveBeenCalledTimes(2); expect(dom?.html()).toMatchInlineSnapshot(` - "
    + "
    basename: /chromeless-a/path html:
    Chromeless A
    " @@ -214,7 +215,8 @@ describe('AppContainer', () => { history={globalHistory} mounters={mockMountersToMounters()} appStatuses$={mountersToAppStatus$()} - setAppLeaveHandler={setAppLeaveHandlerMock} + setAppLeaveHandler={noop} + setIsMounting={noop} /> ); @@ -245,7 +247,8 @@ describe('AppContainer', () => { history={globalHistory} mounters={mockMountersToMounters()} appStatuses$={mountersToAppStatus$()} - setAppLeaveHandler={setAppLeaveHandlerMock} + setAppLeaveHandler={noop} + setIsMounting={noop} /> ); @@ -286,7 +289,8 @@ describe('AppContainer', () => { history={globalHistory} mounters={mockMountersToMounters()} appStatuses$={mountersToAppStatus$()} - setAppLeaveHandler={setAppLeaveHandlerMock} + setAppLeaveHandler={noop} + setIsMounting={noop} /> ); diff --git a/src/core/public/application/integration_tests/utils.tsx b/src/core/public/application/integration_tests/utils.tsx index 9092177da5ad4..fa04b56f83ba1 100644 --- a/src/core/public/application/integration_tests/utils.tsx +++ b/src/core/public/application/integration_tests/utils.tsx @@ -18,6 +18,7 @@ */ import React, { ReactElement } from 'react'; +import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; import { I18nProvider } from '@kbn/i18n/react'; @@ -34,7 +35,9 @@ export const createRenderer = (element: ReactElement | null): Renderer => { return () => new Promise(async resolve => { if (dom) { - dom.update(); + await act(async () => { + dom.update(); + }); } setImmediate(() => resolve(dom)); // flushes any pending promises }); diff --git a/src/core/public/application/ui/app_container.scss b/src/core/public/application/ui/app_container.scss new file mode 100644 index 0000000000000..4f8fec10a97e1 --- /dev/null +++ b/src/core/public/application/ui/app_container.scss @@ -0,0 +1,25 @@ +.appContainer__loading { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: $euiZLevel1; + animation-name: appContainerFadeIn; + animation-iteration-count: 1; + animation-timing-function: ease-in; + animation-duration: 2s; +} + +@keyframes appContainerFadeIn { + 0% { + opacity: 0; + } + + 50% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} diff --git a/src/core/public/application/ui/app_container.test.tsx b/src/core/public/application/ui/app_container.test.tsx index c538227e8f098..2ee71a5bde7dc 100644 --- a/src/core/public/application/ui/app_container.test.tsx +++ b/src/core/public/application/ui/app_container.test.tsx @@ -18,6 +18,7 @@ */ import React from 'react'; +import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; import { AppContainer } from './app_container'; @@ -28,6 +29,12 @@ import { ScopedHistory } from '../scoped_history'; describe('AppContainer', () => { const appId = 'someApp'; const setAppLeaveHandler = jest.fn(); + const setIsMounting = jest.fn(); + + beforeEach(() => { + setAppLeaveHandler.mockClear(); + setIsMounting.mockClear(); + }); const flushPromises = async () => { await new Promise(async resolve => { @@ -67,6 +74,7 @@ describe('AppContainer', () => { appStatus={AppStatus.inaccessible} mounter={mounter} setAppLeaveHandler={setAppLeaveHandler} + setIsMounting={setIsMounting} createScopedHistory={(appPath: string) => // Create a history using the appPath as the current location new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath) @@ -86,10 +94,86 @@ describe('AppContainer', () => { expect(wrapper.text()).toEqual(''); - resolvePromise(); - await flushPromises(); - wrapper.update(); + await act(async () => { + resolvePromise(); + await flushPromises(); + wrapper.update(); + }); expect(wrapper.text()).toContain('some-content'); }); + + it('should call setIsMounting while mounting', async () => { + const [waitPromise, resolvePromise] = createResolver(); + const mounter = createMounter(waitPromise); + + const wrapper = mount( + + // Create a history using the appPath as the current location + new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath) + } + /> + ); + + expect(setIsMounting).toHaveBeenCalledTimes(1); + expect(setIsMounting).toHaveBeenLastCalledWith(true); + + await act(async () => { + resolvePromise(); + await flushPromises(); + wrapper.update(); + }); + + expect(setIsMounting).toHaveBeenCalledTimes(2); + expect(setIsMounting).toHaveBeenLastCalledWith(false); + }); + + it('should call setIsMounting(false) if mounting throws', async () => { + const [waitPromise, resolvePromise] = createResolver(); + const mounter = { + appBasePath: '/base-path', + appRoute: '/some-route', + unmountBeforeMounting: false, + mount: async ({ element }: AppMountParameters) => { + await waitPromise; + throw new Error(`Mounting failed!`); + }, + }; + + const wrapper = mount( + + // Create a history using the appPath as the current location + new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath) + } + /> + ); + + expect(setIsMounting).toHaveBeenCalledTimes(1); + expect(setIsMounting).toHaveBeenLastCalledWith(true); + + // await expect( + await act(async () => { + resolvePromise(); + await flushPromises(); + wrapper.update(); + }); + // ).rejects.toThrow(); + + expect(setIsMounting).toHaveBeenCalledTimes(2); + expect(setIsMounting).toHaveBeenLastCalledWith(false); + }); }); diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index e12a0f2cf2fcd..aad7e6dcf270a 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -26,9 +26,11 @@ import React, { MutableRefObject, } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { AppLeaveHandler, AppStatus, AppUnmount, Mounter } from '../types'; import { AppNotFound } from './app_not_found_screen'; import { ScopedHistory } from '../scoped_history'; +import './app_container.scss'; interface Props { /** Path application is mounted on without the global basePath */ @@ -38,6 +40,7 @@ interface Props { appStatus: AppStatus; setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; createScopedHistory: (appUrl: string) => ScopedHistory; + setIsMounting: (isMounting: boolean) => void; } export const AppContainer: FunctionComponent = ({ @@ -47,7 +50,9 @@ export const AppContainer: FunctionComponent = ({ setAppLeaveHandler, createScopedHistory, appStatus, + setIsMounting, }: Props) => { + const [showSpinner, setShowSpinner] = useState(true); const [appNotFound, setAppNotFound] = useState(false); const elementRef = useRef(null); const unmountRef: MutableRefObject = useRef(null); @@ -65,28 +70,42 @@ export const AppContainer: FunctionComponent = ({ } setAppNotFound(false); + setIsMounting(true); if (mounter.unmountBeforeMounting) { unmount(); } const mount = async () => { - unmountRef.current = - (await mounter.mount({ - appBasePath: mounter.appBasePath, - history: createScopedHistory(appPath), - element: elementRef.current!, - onAppLeave: handler => setAppLeaveHandler(appId, handler), - })) || null; + setShowSpinner(true); + try { + unmountRef.current = + (await mounter.mount({ + appBasePath: mounter.appBasePath, + history: createScopedHistory(appPath), + element: elementRef.current!, + onAppLeave: handler => setAppLeaveHandler(appId, handler), + })) || null; + } catch (e) { + // TODO: add error UI + } finally { + setShowSpinner(false); + setIsMounting(false); + } }; mount(); return unmount; - }, [appId, appStatus, mounter, createScopedHistory, setAppLeaveHandler, appPath]); + }, [appId, appStatus, mounter, createScopedHistory, setAppLeaveHandler, appPath, setIsMounting]); return ( {appNotFound && } + {showSpinner && ( +
    + +
    + )}
    ); diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index 4c135c5769067..ea7c5c9308fe2 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -32,6 +32,7 @@ interface Props { history: History; appStatuses$: Observable>; setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; + setIsMounting: (isMounting: boolean) => void; } interface Params { @@ -43,6 +44,7 @@ export const AppRouter: FunctionComponent = ({ mounters, setAppLeaveHandler, appStatuses$, + setIsMounting, }) => { const appStatuses = useObservable(appStatuses$, new Map()); const createScopedHistory = useMemo( @@ -67,7 +69,7 @@ export const AppRouter: FunctionComponent = ({ appPath={url} appStatus={appStatuses.get(appId) ?? AppStatus.inaccessible} createScopedHistory={createScopedHistory} - {...{ appId, mounter, setAppLeaveHandler }} + {...{ appId, mounter, setAppLeaveHandler, setIsMounting }} /> )} />, @@ -92,7 +94,7 @@ export const AppRouter: FunctionComponent = ({ appId={id} appStatus={appStatuses.get(id) ?? AppStatus.inaccessible} createScopedHistory={createScopedHistory} - {...{ mounter, setAppLeaveHandler }} + {...{ mounter, setAppLeaveHandler, setIsMounting }} /> ); }} From 5e972e14d147e3c5232114690f61a96a493a28ae Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 4 May 2020 13:00:56 -0400 Subject: [PATCH 092/122] [Fleet] Better display of fleet requirements (#65027) --- .../ingest_manager/common/types/index.ts | 1 + .../common/types/rest_spec/fleet_setup.ts | 5 + x-pack/plugins/ingest_manager/kibana.json | 2 +- .../ingest_manager/hooks/use_fleet_status.tsx | 69 ++++++++++++ .../ingest_manager/hooks/use_request/index.ts | 1 + .../ingest_manager/hooks/use_request/setup.ts | 10 +- .../applications/ingest_manager/index.tsx | 5 +- .../agent_enrollment_flyout/index.tsx | 58 +++++++--- .../ingest_manager/sections/fleet/index.tsx | 18 ++- .../sections/fleet/setup_page/index.tsx | 103 ++++++++++++------ .../ingest_manager/types/index.ts | 2 + x-pack/plugins/ingest_manager/server/index.ts | 1 + .../plugins/ingest_manager/server/plugin.ts | 17 ++- .../server/routes/setup/handlers.ts | 47 +++++--- .../server/routes/setup/index.ts | 5 +- .../server/services/agent_config.ts | 10 +- .../server/services/app_context.ts | 27 ++++- .../server/services/datasource.ts | 3 + .../ingest_manager/server/services/output.ts | 5 +- .../ingest_manager/server/services/setup.ts | 7 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 22 files changed, 309 insertions(+), 91 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_fleet_status.tsx diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 748bb14d2d35d..b357d0c2d75f4 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -14,6 +14,7 @@ export interface IngestManagerConfigType { }; fleet: { enabled: boolean; + tlsCheckDisabled: boolean; defaultOutputHost: string; kibana: { host?: string; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts index c4ba8ee595acf..ae4cb4e3fce49 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts @@ -7,3 +7,8 @@ export interface CreateFleetSetupResponse { isInitialized: boolean; } + +export interface GetFleetStatusResponse { + isReady: boolean; + missing_requirements: Array<'tls_required' | 'api_keys' | 'fleet_admin_user'>; +} diff --git a/x-pack/plugins/ingest_manager/kibana.json b/x-pack/plugins/ingest_manager/kibana.json index cef1a293c104b..382ea0444093d 100644 --- a/x-pack/plugins/ingest_manager/kibana.json +++ b/x-pack/plugins/ingest_manager/kibana.json @@ -5,5 +5,5 @@ "ui": true, "configPath": ["xpack", "ingestManager"], "requiredPlugins": ["licensing", "data", "encryptedSavedObjects"], - "optionalPlugins": ["security", "features"] + "optionalPlugins": ["security", "features", "cloud"] } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_fleet_status.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_fleet_status.tsx new file mode 100644 index 0000000000000..ef40c171b9ca3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_fleet_status.tsx @@ -0,0 +1,69 @@ +/* + * 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, { useState, useContext, useEffect } from 'react'; +import { useConfig } from './use_config'; +import { sendGetFleetStatus } from './use_request'; +import { GetFleetStatusResponse } from '../types'; + +interface FleetStatusState { + enabled: boolean; + isLoading: boolean; + isReady: boolean; + missingRequirements?: GetFleetStatusResponse['missing_requirements']; +} + +interface FleetStatus extends FleetStatusState { + refresh: () => Promise; +} + +const FleetStatusContext = React.createContext(undefined); + +export const FleetStatusProvider: React.FC = ({ children }) => { + const config = useConfig(); + const [state, setState] = useState({ + enabled: config.fleet.enabled, + isLoading: false, + isReady: false, + }); + async function sendGetStatus() { + try { + setState(s => ({ ...s, isLoading: true })); + const res = await sendGetFleetStatus(); + if (res.error) { + throw res.error; + } + + setState(s => ({ + ...s, + isLoading: false, + isReady: res.data?.isReady ?? false, + missingRequirements: res.data?.missing_requirements, + })); + } catch (error) { + setState(s => ({ ...s, isLoading: true })); + } + } + useEffect(() => { + sendGetStatus(); + }, []); + + return ( + sendGetStatus() }}> + {children} + + ); +}; + +export function useFleetStatus(): FleetStatus { + const context = useContext(FleetStatusContext); + + if (!context) { + throw new Error('FleetStatusContext not set'); + } + + return context; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts index c39d2a5860bf0..25cdffc5c6651 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts @@ -12,3 +12,4 @@ export * from './enrollment_api_keys'; export * from './epm'; export * from './outputs'; export * from './settings'; +export * from './setup'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/setup.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/setup.ts index 04fdf9f66948f..e4e84e4701f13 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/setup.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/setup.ts @@ -5,7 +5,8 @@ */ import { sendRequest } from './use_request'; -import { setupRouteService } from '../../services'; +import { setupRouteService, fleetSetupRouteService } from '../../services'; +import { GetFleetStatusResponse } from '../../types'; export const sendSetup = () => { return sendRequest({ @@ -13,3 +14,10 @@ export const sendSetup = () => { method: 'post', }); }; + +export const sendGetFleetStatus = () => { + return sendRequest({ + path: fleetSetupRouteService.getFleetSetupPath(), + method: 'get', + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 295a35693726f..f0a0c90a18c24 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -23,6 +23,7 @@ import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp import { CoreContext, DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks'; import { PackageInstallProvider } from './sections/epm/hooks'; import { sendSetup } from './hooks/use_request/setup'; +import { FleetStatusProvider } from './hooks/use_fleet_status'; import './index.scss'; export interface ProtectedRouteProps extends RouteProps { @@ -142,7 +143,9 @@ const IngestManagerApp = ({ - + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx index dd34e7260b27b..e9347ccd2d6c9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx @@ -15,11 +15,15 @@ import { EuiButtonEmpty, EuiButton, EuiFlyoutFooter, + EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentConfig } from '../../../../../types'; import { APIKeySelection } from './key_selection'; import { EnrollmentInstructions } from './instructions'; +import { useFleetStatus } from '../../../../../hooks/use_fleet_status'; +import { useLink } from '../../../../../hooks'; +import { FLEET_PATH } from '../../../../../constants'; interface Props { onClose: () => void; @@ -30,8 +34,11 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ onClose, agentConfigs = [], }) => { + const fleetStatus = useFleetStatus(); const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); + const fleetLink = useLink(FLEET_PATH); + return ( @@ -45,12 +52,33 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ - setSelectedAPIKeyId(keyId)} - /> - - + {fleetStatus.isReady ? ( + <> + setSelectedAPIKeyId(keyId)} + /> + + + + ) : ( + <> + + + + ), + }} + /> + + )} @@ -62,14 +90,16 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ /> - - - - - + {fleetStatus.isReady && ( + + + + + + )} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx index fac81ecc19cd1..b9c5418dbf6f3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx @@ -6,35 +6,31 @@ import React from 'react'; import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; import { Loading } from '../../components'; -import { useConfig, useCore, useRequest } from '../../hooks'; +import { useConfig, useCore } from '../../hooks'; import { AgentListPage } from './agent_list_page'; import { SetupPage } from './setup_page'; import { AgentDetailsPage } from './agent_details_page'; import { NoAccessPage } from './error_pages/no_access'; -import { fleetSetupRouteService } from '../../services'; import { EnrollmentTokenListPage } from './enrollment_token_list_page'; import { ListLayout } from './components/list_layout'; +import { useFleetStatus } from '../../hooks/use_fleet_status'; export const FleetApp: React.FunctionComponent = () => { const core = useCore(); const { fleet } = useConfig(); - const setupRequest = useRequest({ - method: 'get', - path: fleetSetupRouteService.getFleetSetupPath(), - }); + const fleetStatus = useFleetStatus(); if (!fleet.enabled) return null; - if (setupRequest.isLoading) { + if (fleetStatus.isLoading) { return ; } - if (setupRequest.data.isInitialized === false) { + if (fleetStatus.isReady === false) { return ( { - await setupRequest.sendRequest(); - }} + missingRequirements={fleetStatus.missingRequirements || []} + refresh={fleetStatus.refresh} /> ); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx index 96d4d01d67a49..4d89268c14b28 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx @@ -18,10 +18,12 @@ import { import { sendRequest, useCore } from '../../../hooks'; import { fleetSetupRouteService } from '../../../services'; import { WithoutHeaderLayout } from '../../../layouts'; +import { GetFleetStatusResponse } from '../../../types'; export const SetupPage: React.FunctionComponent<{ refresh: () => Promise; -}> = ({ refresh }) => { + missingRequirements: GetFleetStatusResponse['missing_requirements']; +}> = ({ refresh, missingRequirements }) => { const [isFormLoading, setIsFormLoading] = useState(false); const core = useCore(); @@ -40,46 +42,81 @@ export const SetupPage: React.FunctionComponent<{ } }; + const content = + missingRequirements.includes('tls_required') || missingRequirements.includes('api_keys') ? ( + <> + + + + +

    + +

    +
    + + + , + }} + /> + + + + ) : ( + <> + + + + +

    + +

    +
    + + + + + + +
    + + + +
    +
    + + + ); + return ( - + - - - - -

    - -

    -
    - - - - - - -
    - - - -
    -
    - + {content}
    diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 602015d23cefb..ca5bf999aa81a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -20,6 +20,8 @@ export { DatasourceConfigRecordEntry, Output, DataStream, + // API schema - misc setup, status + GetFleetStatusResponse, // API schemas - Agent Config GetAgentConfigsResponse, GetAgentConfigsResponseItem, diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 951ff2337d8c7..6096af8d80801 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -26,6 +26,7 @@ export const config = { }), fleet: schema.object({ enabled: schema.boolean({ defaultValue: true }), + tlsCheckDisabled: schema.boolean({ defaultValue: false }), kibana: schema.object({ host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 3448685d1f279..3b0837565c36c 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -11,6 +11,7 @@ import { Plugin, PluginInitializerContext, SavedObjectsServiceStart, + HttpServerInfo, } from 'kibana/server'; import { LicensingPluginSetup, ILicense } from '../../licensing/server'; import { @@ -42,7 +43,6 @@ import { registerOutputRoutes, registerSettingsRoutes, } from './routes'; - import { IngestManagerConfigType } from '../common'; import { appContextService, @@ -52,12 +52,14 @@ import { AgentService, } from './services'; import { getAgentStatusById } from './services/agents'; +import { CloudSetup } from '../../cloud/server'; export interface IngestManagerSetupDeps { licensing: LicensingPluginSetup; security?: SecurityPluginSetup; features?: FeaturesPluginSetup; encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; + cloud?: CloudSetup; } export type IngestManagerStartDeps = object; @@ -67,6 +69,9 @@ export interface IngestManagerAppContext { security?: SecurityPluginSetup; config$?: Observable; savedObjects: SavedObjectsServiceStart; + isProductionMode: boolean; + serverInfo?: HttpServerInfo; + cloud?: CloudSetup; } export type IngestManagerSetupContract = void; @@ -100,16 +105,23 @@ export class IngestManagerPlugin private licensing$!: Observable; private config$: Observable; private security: SecurityPluginSetup | undefined; + private cloud: CloudSetup | undefined; + + private isProductionMode: boolean; + private serverInfo: HttpServerInfo | undefined; constructor(private readonly initializerContext: PluginInitializerContext) { this.config$ = this.initializerContext.config.create(); + this.isProductionMode = this.initializerContext.env.mode.prod; } public async setup(core: CoreSetup, deps: IngestManagerSetupDeps) { + this.serverInfo = core.http.getServerInfo(); this.licensing$ = deps.licensing.license$; if (deps.security) { this.security = deps.security; } + this.cloud = deps.cloud; registerSavedObjects(core.savedObjects); registerEncryptedSavedObjects(deps.encryptedSavedObjects); @@ -184,6 +196,9 @@ export class IngestManagerPlugin security: this.security, config$: this.config$, savedObjects: core.savedObjects, + isProductionMode: this.isProductionMode, + serverInfo: this.serverInfo, + cloud: this.cloud, }); licenseService.start(this.licensing$); return { diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts index 837e73b966feb..542dfa9cefe8f 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts @@ -4,28 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ import { RequestHandler } from 'src/core/server'; -import { outputService } from '../../services'; -import { CreateFleetSetupResponse } from '../../../common'; +import { outputService, appContextService } from '../../services'; +import { GetFleetStatusResponse } from '../../../common'; import { setupIngestManager, setupFleet } from '../../services/setup'; -export const getFleetSetupHandler: RequestHandler = async (context, request, response) => { +export const getFleetStatusHandler: RequestHandler = async (context, request, response) => { const soClient = context.core.savedObjects.client; - const successBody: CreateFleetSetupResponse = { isInitialized: true }; - const failureBody: CreateFleetSetupResponse = { isInitialized: false }; try { - const adminUser = await outputService.getAdminUser(soClient); - if (adminUser) { - return response.ok({ - body: successBody, - }); - } else { - return response.ok({ - body: failureBody, - }); + const isAdminUserSetup = (await outputService.getAdminUser(soClient)) !== null; + const isApiKeysEnabled = await appContextService.getSecurity().authc.areAPIKeysEnabled(); + const isTLSEnabled = appContextService.getServerInfo().protocol === 'https'; + const isProductionMode = appContextService.getIsProductionMode(); + const isCloud = appContextService.getCloud()?.isCloudEnabled ?? false; + const isTLSCheckDisabled = appContextService.getConfig()?.fleet?.tlsCheckDisabled ?? false; + + const missingRequirements: GetFleetStatusResponse['missing_requirements'] = []; + if (!isAdminUserSetup) { + missingRequirements.push('fleet_admin_user'); } - } catch (e) { + if (!isApiKeysEnabled) { + missingRequirements.push('api_keys'); + } + if (!isTLSCheckDisabled && !isCloud && isProductionMode && !isTLSEnabled) { + missingRequirements.push('tls_required'); + } + + const body: GetFleetStatusResponse = { + isReady: missingRequirements.length === 0, + missing_requirements: missingRequirements, + }; + return response.ok({ - body: failureBody, + body, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, }); } }; diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts index 5ee7ee7733220..43dcf47d26c18 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { IRouter } from 'src/core/server'; + import { PLUGIN_ID, FLEET_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; import { IngestManagerConfigType } from '../../../common'; import { - getFleetSetupHandler, + getFleetStatusHandler, createFleetSetupHandler, ingestManagerSetupHandler, } from './handlers'; @@ -36,7 +37,7 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) validate: false, options: { tags: [`access:${PLUGIN_ID}-read`] }, }, - getFleetSetupHandler + getFleetStatusHandler ); // Create Fleet setup diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index 5ecbaff8ad71e..84bcd7db3f7b1 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -314,10 +314,12 @@ class AgentConfigService { if (!config) { return null; } - const defaultOutput = await outputService.get( - soClient, - await outputService.getDefaultOutputId(soClient) - ); + + const defaultOutputId = await outputService.getDefaultOutputId(soClient); + if (!defaultOutputId) { + throw new Error('Default output is not setup'); + } + const defaultOutput = await outputService.get(soClient, defaultOutputId); const agentConfig: FullAgentConfig = { id: config.id, diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index e917d2edd1309..5e538ad84b4c2 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -5,11 +5,12 @@ */ import { BehaviorSubject, Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { SavedObjectsServiceStart } from 'src/core/server'; +import { SavedObjectsServiceStart, HttpServerInfo } from 'src/core/server'; import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../../security/server'; import { IngestManagerConfigType } from '../../common'; import { IngestManagerAppContext } from '../plugin'; +import { CloudSetup } from '../../../cloud/server'; class AppContextService { private encryptedSavedObjects: EncryptedSavedObjectsPluginStart | undefined; @@ -17,11 +18,17 @@ class AppContextService { private config$?: Observable; private configSubject$?: BehaviorSubject; private savedObjects: SavedObjectsServiceStart | undefined; + private serverInfo: HttpServerInfo | undefined; + private isProductionMode: boolean = false; + private cloud?: CloudSetup; public async start(appContext: IngestManagerAppContext) { this.encryptedSavedObjects = appContext.encryptedSavedObjects; this.security = appContext.security; this.savedObjects = appContext.savedObjects; + this.serverInfo = appContext.serverInfo; + this.isProductionMode = appContext.isProductionMode; + this.cloud = appContext.cloud; if (appContext.config$) { this.config$ = appContext.config$; @@ -41,9 +48,16 @@ class AppContextService { } public getSecurity() { + if (!this.security) { + throw new Error('Secury service not set.'); + } return this.security; } + public getCloud() { + return this.cloud; + } + public getConfig() { return this.configSubject$?.value; } @@ -58,6 +72,17 @@ class AppContextService { } return this.savedObjects; } + + public getIsProductionMode() { + return this.isProductionMode; + } + + public getServerInfo() { + if (!this.serverInfo) { + throw new Error('Server info not set.'); + } + return this.serverInfo; + } } export const appContextService = new AppContextService(); diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts index affd9b2755881..0497bc5a2b541 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.ts @@ -196,6 +196,9 @@ class DatasourceService { outputService.getDefaultOutputId(soClient), ]); if (pkgInfo) { + if (!defaultOutputId) { + throw new Error('Default output is not set'); + } return packageToConfigDatasource(pkgInfo, '', defaultOutputId); } } diff --git a/x-pack/plugins/ingest_manager/server/services/output.ts b/x-pack/plugins/ingest_manager/server/services/output.ts index 395c9af4a4ca2..3628c5bd9e183 100644 --- a/x-pack/plugins/ingest_manager/server/services/output.ts +++ b/x-pack/plugins/ingest_manager/server/services/output.ts @@ -48,7 +48,7 @@ class OutputService { }); if (!outputs.saved_objects.length) { - throw new Error('No default output'); + return null; } return outputs.saved_objects[0].id; @@ -56,6 +56,9 @@ class OutputService { public async getAdminUser(soClient: SavedObjectsClientContract) { const defaultOutputId = await this.getDefaultOutputId(soClient); + if (!defaultOutputId) { + return null; + } const so = await appContextService .getEncryptedSavedObjects() ?.getDecryptedAsInternalUser(OUTPUT_SAVED_OBJECT_TYPE, defaultOutputId); diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 390e240841611..3619628bd4f8b 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -109,7 +109,12 @@ export async function setupFleet( }); // save fleet admin user - await outputService.updateOutput(soClient, await outputService.getDefaultOutputId(soClient), { + const defaultOutputId = await outputService.getDefaultOutputId(soClient); + if (!defaultOutputId) { + throw new Error('Default output does not exist'); + } + + await outputService.updateOutput(soClient, defaultOutputId, { fleet_enroll_username: FLEET_ENROLL_USERNAME, fleet_enroll_password: password, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index da8673da67f42..11d1f864a3619 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8370,9 +8370,7 @@ "xpack.ingestManager.noAccess.accessDeniedTitle": "アクセスが拒否されました", "xpack.ingestManager.overviewPageSubtitle": "Ingest Manager についてのロレムイプサム説明文。", "xpack.ingestManager.overviewPageTitle": "Ingest Manager", - "xpack.ingestManager.setupPage.description": "フリートを使用するには、Elastic ユーザーを作成する必要があります。このユーザーは、API キーを作成して、logs-* および metrics-* に書き込むことができます。", "xpack.ingestManager.setupPage.enableFleet": "ユーザーを作成してフリートを有効にます", - "xpack.ingestManager.setupPage.title": "フリートを有効にする", "xpack.ingestManager.unenrollAgents.confirmModal.cancelButtonLabel": "キャンセル", "xpack.ingestManager.unenrollAgents.confirmModal.confirmButtonLabel": "登録解除", "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "{count, plural, one {# エージェント} other {# エージェント}}の登録を解除しますか?", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f66e9631b0168..9ee3b3d8f5931 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8376,9 +8376,7 @@ "xpack.ingestManager.noAccess.accessDeniedTitle": "访问被拒绝", "xpack.ingestManager.overviewPageSubtitle": "Lorem ipsum some description about ingest manager.", "xpack.ingestManager.overviewPageTitle": "Ingest Manager", - "xpack.ingestManager.setupPage.description": "要使用 Fleet,必须创建 Elastic 用户。此用户可以创建 API 密钥并写入到 logs-* and metrics-*。", "xpack.ingestManager.setupPage.enableFleet": "创建用户并启用 Fleet", - "xpack.ingestManager.setupPage.title": "启用 Fleet", "xpack.ingestManager.unenrollAgents.confirmModal.cancelButtonLabel": "取消", "xpack.ingestManager.unenrollAgents.confirmModal.confirmButtonLabel": "取消注册", "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "取消注册 {count, plural, one {# 个代理} other {# 个代理}}?", From 122450a4c889f50fc8a96ad30d849b7a260ad025 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 4 May 2020 18:04:42 +0100 Subject: [PATCH 093/122] chore(NA): skip functional test for visualize axis scalling preventing es snapshot promotion (#65100) --- test/functional/apps/visualize/_area_chart.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/visualize/_area_chart.js b/test/functional/apps/visualize/_area_chart.js index 8f2012d7f184d..05544029f62d7 100644 --- a/test/functional/apps/visualize/_area_chart.js +++ b/test/functional/apps/visualize/_area_chart.js @@ -242,7 +242,9 @@ export default function({ getService, getPageObjects }) { await inspector.close(); }); - it('does not scale top hit agg', async () => { + // Preventing ES Promotion for master (8.0) + // https://github.com/elastic/kibana/issues/64734 + it.skip('does not scale top hit agg', async () => { const expectedTableData = [ ['2015-09-20 00:00', '6', '9.035KB'], ['2015-09-20 01:00', '9', '5.854KB'], From 9db27dba56c245a118fd03ffa4e31475195ffbfe Mon Sep 17 00:00:00 2001 From: Ross Wolf <31489089+rw-access@users.noreply.github.com> Date: Mon, 4 May 2020 11:07:09 -0600 Subject: [PATCH 094/122] [SIEM] Remove forgotten rules that weren't deleted (#64974) * Remove stray rules that should've been deleted * Update rule.ts and tests * Remove deleted prebuilt rules from cypress ES archive (#1) --- x-pack/plugins/siem/cypress/objects/rule.ts | 2 +- .../rules/prepackaged_rules/index.ts | 39 ++++++------- .../windows_execution_via_regsvr32.json | 51 ----------------- ...windows_signed_binary_proxy_execution.json | 54 ------------------ ...uspicious_process_started_by_a_script.json | 54 ------------------ .../prebuilt_rules_loaded/data.json.gz | Bin 41865 -> 41851 bytes 6 files changed, 18 insertions(+), 182 deletions(-) delete mode 100644 x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_regsvr32.json delete mode 100644 x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_signed_binary_proxy_execution.json delete mode 100644 x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_process_started_by_a_script.json diff --git a/x-pack/plugins/siem/cypress/objects/rule.ts b/x-pack/plugins/siem/cypress/objects/rule.ts index ce920aeb957af..4e0189ea597da 100644 --- a/x-pack/plugins/siem/cypress/objects/rule.ts +++ b/x-pack/plugins/siem/cypress/objects/rule.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const totalNumberOfPrebuiltRules = 130; +export const totalNumberOfPrebuiltRules = 127; interface Mitre { tactic: string; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts index c24f5bb64ef5e..9e185b5a5ef7c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/index.ts @@ -118,25 +118,23 @@ import rule108 from './windows_execution_msbuild_started_renamed.json'; import rule109 from './windows_execution_msbuild_started_unusal_process.json'; import rule110 from './windows_execution_via_compiled_html_file.json'; import rule111 from './windows_execution_via_net_com_assemblies.json'; -import rule112 from './windows_execution_via_regsvr32.json'; -import rule113 from './windows_execution_via_trusted_developer_utilities.json'; -import rule114 from './windows_html_help_executable_program_connecting_to_the_internet.json'; -import rule115 from './windows_injection_msbuild.json'; -import rule116 from './windows_misc_lolbin_connecting_to_the_internet.json'; -import rule117 from './windows_modification_of_boot_config.json'; -import rule118 from './windows_msxsl_network.json'; -import rule119 from './windows_net_command_system_account.json'; -import rule120 from './windows_persistence_via_application_shimming.json'; -import rule121 from './windows_priv_escalation_via_accessibility_features.json'; -import rule122 from './windows_process_discovery_via_tasklist_command.json'; -import rule123 from './windows_rare_user_runas_event.json'; -import rule124 from './windows_rare_user_type10_remote_login.json'; -import rule125 from './windows_register_server_program_connecting_to_the_internet.json'; -import rule126 from './windows_signed_binary_proxy_execution.json'; -import rule127 from './windows_suspicious_pdf_reader.json'; -import rule128 from './windows_suspicious_process_started_by_a_script.json'; -import rule129 from './windows_uac_bypass_event_viewer.json'; -import rule130 from './windows_whoami_command_activity.json'; +import rule112 from './windows_execution_via_trusted_developer_utilities.json'; +import rule113 from './windows_html_help_executable_program_connecting_to_the_internet.json'; +import rule114 from './windows_injection_msbuild.json'; +import rule115 from './windows_misc_lolbin_connecting_to_the_internet.json'; +import rule116 from './windows_modification_of_boot_config.json'; +import rule117 from './windows_msxsl_network.json'; +import rule118 from './windows_net_command_system_account.json'; +import rule119 from './windows_persistence_via_application_shimming.json'; +import rule120 from './windows_priv_escalation_via_accessibility_features.json'; +import rule121 from './windows_process_discovery_via_tasklist_command.json'; +import rule122 from './windows_rare_user_runas_event.json'; +import rule123 from './windows_rare_user_type10_remote_login.json'; +import rule124 from './windows_register_server_program_connecting_to_the_internet.json'; +import rule125 from './windows_suspicious_pdf_reader.json'; +import rule126 from './windows_uac_bypass_event_viewer.json'; +import rule127 from './windows_whoami_command_activity.json'; + export const rawRules = [ rule1, rule2, @@ -265,7 +263,4 @@ export const rawRules = [ rule125, rule126, rule127, - rule128, - rule129, - rule130, ]; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_regsvr32.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_regsvr32.json deleted file mode 100644 index e8e7ddfc168dc..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_execution_via_regsvr32.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "description": "Identifies scrobj.dll loaded into unusual Microsoft processes. This may indicate a malicious scriptlet is being executed in the target process.", - "index": [ - "winlogbeat-*" - ], - "language": "kuery", - "max_signals": 100, - "name": "Suspicious Script Object Execution", - "query": "event.code: 1 and scrobj.dll and (process.name:certutil.exe or process.name:regsvr32.exe or process.name:rundll32.exe)", - "risk_score": 21, - "rule_id": "b7333d08-be4b-4cb4-b81e-924ae37b3143", - "severity": "low", - "tags": [ - "Elastic", - "Windows" - ], - "threat": [ - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0005", - "name": "Defense Evasion", - "reference": "https://attack.mitre.org/tactics/TA0005/" - }, - "technique": [ - { - "id": "T1064", - "name": "Scripting", - "reference": "https://attack.mitre.org/techniques/T1064/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0002", - "name": "Execution", - "reference": "https://attack.mitre.org/tactics/TA0002/" - }, - "technique": [ - { - "id": "T1064", - "name": "Scripting", - "reference": "https://attack.mitre.org/techniques/T1064/" - } - ] - } - ], - "type": "query", - "version": 1 -} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_signed_binary_proxy_execution.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_signed_binary_proxy_execution.json deleted file mode 100644 index be4ccef2a0887..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_signed_binary_proxy_execution.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "description": "Binaries signed with trusted digital certificates can execute on Windows systems protected by digital signature validation. Adversaries may use these binaries to 'live off the land' and execute malicious files that could bypass application whitelisting and signature validation.", - "false_positives": [ - "Security testing may produce events like this. Activity of this kind performed by non-engineers and ordinary users is unusual." - ], - "index": [ - "winlogbeat-*" - ], - "language": "kuery", - "max_signals": 100, - "name": "Execution via Signed Binary", - "query": "event.code:1 and http and (process.name:certutil.exe or process.name:msiexec.exe)", - "risk_score": 21, - "rule_id": "7edb573f-1f9b-4161-8c19-c7c383bb17f2", - "severity": "low", - "tags": [ - "Elastic", - "Windows" - ], - "threat": [ - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0005", - "name": "Defense Evasion", - "reference": "https://attack.mitre.org/tactics/TA0005/" - }, - "technique": [ - { - "id": "T1218", - "name": "Signed Binary Proxy Execution", - "reference": "https://attack.mitre.org/techniques/T1218/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0002", - "name": "Execution", - "reference": "https://attack.mitre.org/tactics/TA0002/" - }, - "technique": [ - { - "id": "T1218", - "name": "Signed Binary Proxy Execution", - "reference": "https://attack.mitre.org/techniques/T1218/" - } - ] - } - ], - "type": "query", - "version": 1 -} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_process_started_by_a_script.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_process_started_by_a_script.json deleted file mode 100644 index 235a04f8063fc..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/windows_suspicious_process_started_by_a_script.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "description": "Identifies a suspicious process being spawned from a script interpreter, which could be indicative of a potential phishing attack.", - "false_positives": [ - "Security testing may produce events like this. Activity of this kind performed by non-engineers and ordinary users is unusual." - ], - "index": [ - "winlogbeat-*" - ], - "language": "kuery", - "max_signals": 100, - "name": "Suspicious Process spawning from Script Interpreter", - "query": "(process.parent.name:cmd.exe or process.parent.name:cscript.exe or process.parent.name:mshta.exe or process.parent.name:powershell.exe or process.parent.name:rundll32.exe or process.parent.name:wscript.exe or process.parent.name:wmiprvse.exe) and (process.name:bitsadmin.exe or process.name:certutil.exe or mshta.exe or process.name:nslookup.exe or process.name:schtasks.exe) and event.code:1", - "risk_score": 21, - "rule_id": "89db767d-99f9-479f-8052-9205fd3090c4", - "severity": "low", - "tags": [ - "Elastic", - "Windows" - ], - "threat": [ - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0005", - "name": "Defense Evasion", - "reference": "https://attack.mitre.org/tactics/TA0005/" - }, - "technique": [ - { - "id": "T1064", - "name": "Scripting", - "reference": "https://attack.mitre.org/techniques/T1064/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0002", - "name": "Execution", - "reference": "https://attack.mitre.org/tactics/TA0002/" - }, - "technique": [ - { - "id": "T1064", - "name": "Scripting", - "reference": "https://attack.mitre.org/techniques/T1064/" - } - ] - } - ], - "type": "query", - "version": 1 -} diff --git a/x-pack/test/siem_cypress/es_archives/prebuilt_rules_loaded/data.json.gz b/x-pack/test/siem_cypress/es_archives/prebuilt_rules_loaded/data.json.gz index 573c006d1507d9c84c581c9abab7d60d96bfebe3..cac63ed9c585f993b805b80657b9888a40f7d819 100644 GIT binary patch literal 41851 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 Date: Mon, 4 May 2020 10:07:23 -0700 Subject: [PATCH 095/122] [Canvas] Updates function reference docs (#64741) --- .../canvas/canvas-function-reference.asciidoc | 271 +++++++++++++++++- 1 file changed, 256 insertions(+), 15 deletions(-) diff --git a/docs/canvas/canvas-function-reference.asciidoc b/docs/canvas/canvas-function-reference.asciidoc index 16aaf55802b17..657e3ec8b8bb1 100644 --- a/docs/canvas/canvas-function-reference.asciidoc +++ b/docs/canvas/canvas-function-reference.asciidoc @@ -42,10 +42,10 @@ filters | metric "Average uptime" metricFont={ font size=48 family="'Open Sans', Helvetica, Arial, sans-serif" - color={ - if {all {gte 0} {lt 0.8}} then="red" else="green" - } - align="center" lHeight=48 + color={ + if {all {gte 0} {lt 0.8}} then="red" else="green" + } + align="center" lHeight=48 } | render ---- @@ -324,12 +324,14 @@ case if={lte 50} then="green" ---- math "random()" | progress shape="gauge" label={formatnumber "0%"} - font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" align="center" - color={ - switch {case if={lte 0.5} then="green"} - {case if={all {gt 0.5} {lte 0.75}} then="orange"} - default="red" - }} + font={ + font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" align="center" + color={ + switch {case if={lte 0.5} then="green"} + {case if={all {gt 0.5} {lte 0.75}} then="orange"} + default="red" + } + } valueColor={ switch {case if={lte 0.5} then="green"} {case if={all {gt 0.5} {lte 0.75}} then="orange"} @@ -693,7 +695,25 @@ Alias: `value` [[demodata_fn]] === `demodata` -A mock data set that includes project CI times with usernames, countries, and run phases. +A sample data set that includes project CI times with usernames, countries, and run phases. + +*Expression syntax* +[source,js] +---- +demodata +demodata "ci" +demodata type="shirts" +---- + +*Code example* +[source,text] +---- +filters +| demodata +| table +| render +---- +`demodata` is a mock data set that you can use to start playing around in Canvas. *Accepts:* `filter` @@ -837,6 +857,28 @@ Alias: `value` Query Elasticsearch for the number of hits matching the specified query. +*Expression syntax* +[source,js] +---- +escount index="logstash-*" +escount "currency:"EUR"" index="kibana_sample_data_ecommerce" +escount query="response:404" index="kibana_sample_data_logs" +---- + +*Code example* +[source,text] +---- +filters +| escount "Cancelled:true" index="kibana_sample_data_flights" +| math "value" +| progress shape="semicircle" + label={formatnumber 0,0} + font={font size=24 family="'Open Sans', Helvetica, Arial, sans-serif" color="#000000" align=center} + max={filters | escount index="kibana_sample_data_flights"} +| render +---- +The first `escount` expression retrieves the number of flights that were cancelled. The second `escount` expression retrieves the total number of flights. + *Accepts:* `filter` [cols="3*^<"] @@ -867,6 +909,34 @@ Default: `_all` Query Elasticsearch for raw documents. Specify the fields you want to retrieve, especially if you are asking for a lot of rows. +*Expression syntax* +[source,js] +---- +esdocs index="logstash-*" +esdocs "currency:"EUR"" index="kibana_sample_data_ecommerce" +esdocs query="response:404" index="kibana_sample_data_logs" +esdocs index="kibana_sample_data_flights" count=100 +esdocs index="kibana_sample_data_flights" sort="AvgTicketPrice, asc" +---- + +*Code example* +[source,text] +---- +filters +| esdocs index="kibana_sample_data_ecommerce" + fields="customer_gender, taxful_total_price, order_date" + sort="order_date, asc" + count=10000 +| mapColumn "order_date" + fn={getCell "order_date" | date {context} | rounddate "YYYY-MM-DD"} +| alterColumn "order_date" type="date" +| pointseries x="order_date" y="sum(taxful_total_price)" color="customer_gender" +| plot defaultStyle={seriesStyle lines=3} + palette={palette "#7ECAE3" "#003A4D" gradient=true} +| render +---- +This retrieves the first 10000 documents data from the `kibana_sample_data_ecommerce` index sorted by `order_date` in ascending order, and only requests the `customer_gender`, `taxful_total_price`, and `order_date` fields. + *Accepts:* `filter` [cols="3*^<"] @@ -915,6 +985,23 @@ Default: `_all` Queries Elasticsearch using Elasticsearch SQL. +*Expression syntax* +[source,js] +---- +essql query="SELECT * FROM "logstash*"" +essql "SELECT * FROM "apm*"" count=10000 +---- + +*Code example* +[source,text] +---- +filters +| essql query="SELECT Carrier, FlightDelayMin, AvgTicketPrice FROM "kibana_sample_data_flights"" +| table +| render +---- +This retrieves the `Carrier`, `FlightDelayMin`, and `AvgTicketPrice` fields from the "kibana_sample_data_flights" index. + *Accepts:* `filter` [cols="3*^<"] @@ -1107,7 +1194,7 @@ Default: `false` [[font_fn]] === `font` -Creates a font style. +Create a font style. *Expression syntax* [source,js] @@ -1244,7 +1331,7 @@ Alias: `format` [[formatnumber_fn]] === `formatnumber` -Formats a number into a formatted number string using the <>. +Formats a number into a formatted number string using the Numeral pattern. *Expression syntax* [source,js] @@ -1276,7 +1363,7 @@ The `formatnumber` subexpression receives the same `context` as the `progress` f Alias: `format` |`string` -|A <> string. For example, `"0.0a"` or `"0%"`. +|A Numeral pattern format string. For example, `"0.0a"` or `"0%"`. |=== *Returns:* `string` @@ -1559,6 +1646,34 @@ Alias: `value` [[m_fns]] == M +[float] +[[mapCenter_fn]] +=== `mapCenter` + +Returns an object with the center coordinates and zoom level of the map. + +*Accepts:* `null` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|`lat` *** +|`number` +|Latitude for the center of the map + +|`lon` *** +|`number` +|Longitude for the center of the map + +|`zoom` *** +|`number` +|Zoom level of the map +|=== + +*Returns:* `mapCenter` + + [float] [[mapColumn_fn]] === `mapColumn` @@ -1612,6 +1727,12 @@ Default: `""` |The CSS font properties for the content. For example, "font-family" or "font-weight". Default: `${font}` + +|`openLinksInNewTab` +|`boolean` +|A true or false value for opening links in a new tab. The default value is `false`. Setting to `true` opens all links in a new tab. + +Default: `false` |=== *Returns:* `render` @@ -1675,7 +1796,7 @@ Default: `${font size=48 family="'Open Sans', Helvetica, Arial, sans-serif" colo Alias: `format` |`string` -|A <> string. For example, `"0.0a"` or `"0%"`. +|A Numeral pattern format string. For example, `"0.0a"` or `"0%"`. |=== *Returns:* `render` @@ -2184,6 +2305,102 @@ Returns the number of rows. Pairs with <> to get the count of unique col [[s_fns]] == S +[float] +[[savedLens_fn]] +=== `savedLens` + +Returns an embeddable for a saved Lens visualization object. + +*Accepts:* `any` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|`id` +|`string` +|The ID of the saved Lens visualization object + +|`timerange` +|`timerange` +|The timerange of data that should be included + +|`title` +|`string` +|The title for the Lens visualization object +|=== + +*Returns:* `embeddable` + + +[float] +[[savedMap_fn]] +=== `savedMap` + +Returns an embeddable for a saved map object. + +*Accepts:* `any` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|`center` +|`mapCenter` +|The center and zoom level the map should have + +|`hideLayer` † +|`string` +|The IDs of map layers that should be hidden + +|`id` +|`string` +|The ID of the saved map object + +|`timerange` +|`timerange` +|The timerange of data that should be included + +|`title` +|`string` +|The title for the map +|=== + +*Returns:* `embeddable` + + +[float] +[[savedVisualization_fn]] +=== `savedVisualization` + +Returns an embeddable for a saved visualization object. + +*Accepts:* `any` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|`colors` † +|`seriesStyle` +|Defines the color to use for a specific series + +|`hideLegend` +|`boolean` +|Specifies the option to hide the legend + +|`id` +|`string` +|The ID of the saved visualization object + +|`timerange` +|`timerange` +|The timerange of data that should be included +|=== + +*Returns:* `embeddable` + + [float] [[seriesStyle_fn]] === `seriesStyle` @@ -2579,6 +2796,30 @@ Default: `"now"` *Returns:* `datatable` +[float] +[[timerange_fn]] +=== `timerange` + +An object that represents a span of time. + +*Accepts:* `null` + +[cols="3*^<"] +|=== +|Argument |Type |Description + +|`from` *** +|`string` +|The start of the time range + +|`to` *** +|`string` +|The end of the time range +|=== + +*Returns:* `timerange` + + [float] [[to_fn]] === `to` From f62df99ae38801b905dd10d281dbf972c5b17578 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 4 May 2020 19:09:53 +0200 Subject: [PATCH 096/122] [ML] Embeddable Anomaly Swimlane (#64056) * [ML] embeddables setup * [ML] fix initialization * [ML] ts refactoring * [ML] refactor time_buckets.js * [ML] async services * [ML] extract job_selector_flyout.tsx * [ML] fetch overall swimlane data * [ML] import explorer styles * [ML] revert package_globs.ts * [ML] refactor with container, services with DI * [ML] resize throttle, fetch based on chart width * [ML] swimlane embeddable setup * [ML] explorer service * [ML] chart_tooltip_service * [ML] fix types * [ML] overall type for single job with no influencers * [ML] improve anomaly_swimlane_initializer ux * [ML] fix services initialization, unsubscribe on destroy * [ML] support custom time range * [ML] add tooltip * [ML] rollback initGetSwimlaneBucketInterval * [ML] new tooltip service * [ML] MlTooltipComponent with render props, fix warning * [ML] fix typo in the filename * [ML] remove redundant time range output * [ML] fix time_buckets.test.js jest tests * [ML] fix explorer chart tests * [ML] swimlane tests * [ML] store job ids instead of complete job objects * [ML] swimlane limit input * [ML] memo tooltip component, loading indicator * [ML] scrollable content * [ML] support query and filters * [ML] handle query syntax errors * [ML] rename anomaly_swimlane_service * [ML] introduce constants * [ML] edit panel title during setup * [ML] withTimeRangeSelector * [ML] rename explorer_service * [ML] getJobs$ method with one API call * [ML] fix groups selection * [ML] swimlane input resolver hook * [ML] useSwimlaneInputResolver tests * [ML] factory test * [ML] container test * [ML] set wrapper * [ML] tooltip tests * [ML] fix displayScore * [ML] label colors * [ML] support edit mode * [ML] call super render Co-authored-by: Elastic Machine --- x-pack/plugins/ml/kibana.json | 4 +- .../chart_tooltip/chart_tooltip.tsx | 182 +++++----- .../chart_tooltip/chart_tooltip_service.d.ts | 42 --- .../chart_tooltip/chart_tooltip_service.js | 37 -- .../chart_tooltip_service.test.ts | 63 +++- .../chart_tooltip/chart_tooltip_service.ts | 73 ++++ .../components/chart_tooltip/index.ts | 4 +- .../components/job_selector/job_selector.tsx | 280 ++-------------- .../job_selector_badge/{index.js => index.ts} | 0 ...lector_badge.js => job_selector_badge.tsx} | 35 +- .../job_selector/job_selector_flyout.tsx | 289 ++++++++++++++++ .../job_selector_table/job_selector_table.js | 2 +- .../{index.js => index.ts} | 0 ..._badges.js => new_selection_id_badges.tsx} | 32 +- .../datavisualizer/index_based/page.tsx | 4 +- ...s.snap => explorer_swimlane.test.tsx.snap} | 0 .../application/explorer/_explorer.scss | 316 +++++++++--------- .../public/application/explorer/explorer.js | 63 ++-- .../explorer_chart_distribution.js | 12 +- .../explorer_chart_distribution.test.js | 34 +- .../explorer_chart_single_metric.js | 14 +- .../explorer_chart_single_metric.test.js | 34 +- .../explorer_charts_container.js | 109 +++--- .../explorer_charts_container.test.js | 12 +- .../explorer/explorer_constants.ts | 10 +- .../explorer/explorer_dashboard_service.ts | 7 +- ...ane.test.js => explorer_swimlane.test.tsx} | 50 ++- ...orer_swimlane.js => explorer_swimlane.tsx} | 253 ++++++++------ .../application/explorer/explorer_utils.d.ts | 10 +- .../application/explorer/explorer_utils.js | 4 +- .../components/charts/common/settings.ts | 4 +- .../jobs/new_job/pages/new_job/page.tsx | 4 +- .../services/anomaly_detector_service.ts | 58 ++++ .../application/services/explorer_service.ts | 308 +++++++++++++++++ .../application/services/http_service.ts | 104 +++++- .../services/results_service/index.ts | 2 + .../results_service/results_service.d.ts | 11 +- .../timeseries_chart/timeseries_chart.d.ts | 2 + .../timeseries_chart/timeseries_chart.js | 23 +- .../timeseries_chart_annotations.ts | 8 +- .../timeseriesexplorer/timeseriesexplorer.js | 30 +- .../timeseriesexplorer_utils.js | 6 +- .../public/application/util/chart_utils.d.ts | 7 + .../ml/public/application/util/date_utils.ts | 2 +- .../public/application/util/time_buckets.d.ts | 37 +- .../public/application/util/time_buckets.js | 26 +- .../application/util/time_buckets.test.js | 28 +- .../anomaly_swimlane_embeddable.tsx | 115 +++++++ ...omaly_swimlane_embeddable_factory.test.tsx | 50 +++ .../anomaly_swimlane_embeddable_factory.ts | 81 +++++ .../anomaly_swimlane_initializer.tsx | 201 +++++++++++ .../anomaly_swimlane_setup_flyout.tsx | 95 ++++++ .../explorer_swimlane_container.test.tsx | 124 +++++++ .../explorer_swimlane_container.tsx | 122 +++++++ .../embeddables/anomaly_swimlane/index.ts | 8 + .../swimlane_input_resolver.test.ts | 271 +++++++++++++++ .../swimlane_input_resolver.ts | 211 ++++++++++++ x-pack/plugins/ml/public/embeddables/index.ts | 23 ++ x-pack/plugins/ml/public/plugin.ts | 13 + .../ui_actions/edit_swimlane_panel_action.tsx | 65 ++++ x-pack/plugins/ml/public/ui_actions/index.ts | 30 ++ 61 files changed, 3100 insertions(+), 944 deletions(-) delete mode 100644 x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.d.ts delete mode 100644 x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.js create mode 100644 x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.ts rename x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/{index.js => index.ts} (100%) rename x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/{job_selector_badge.js => job_selector_badge.tsx} (68%) create mode 100644 x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx rename x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/{index.js => index.ts} (100%) rename x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/{new_selection_id_badges.js => new_selection_id_badges.tsx} (80%) rename x-pack/plugins/ml/public/application/explorer/__snapshots__/{explorer_swimlane.test.js.snap => explorer_swimlane.test.tsx.snap} (100%) rename x-pack/plugins/ml/public/application/explorer/{explorer_swimlane.test.js => explorer_swimlane.test.tsx} (70%) rename x-pack/plugins/ml/public/application/explorer/{explorer_swimlane.js => explorer_swimlane.tsx} (79%) create mode 100644 x-pack/plugins/ml/public/application/services/anomaly_detector_service.ts create mode 100644 x-pack/plugins/ml/public/application/services/explorer_service.ts create mode 100644 x-pack/plugins/ml/public/application/util/chart_utils.d.ts create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.test.tsx create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/index.ts create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts create mode 100644 x-pack/plugins/ml/public/embeddables/index.ts create mode 100644 x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx create mode 100644 x-pack/plugins/ml/public/ui_actions/index.ts diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 038f61b3a33b7..e9d4aff3484b1 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -13,7 +13,9 @@ "home", "licensing", "usageCollection", - "share" + "share", + "embeddable", + "uiActions" ], "optionalPlugins": [ "security", diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx index 9cc42a4df2f66..decd1275fe884 100644 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx @@ -4,56 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; -import React, { useRef, FC } from 'react'; +import TooltipTrigger from 'react-popper-tooltip'; import { TooltipValueFormatter } from '@elastic/charts'; -import useObservable from 'react-use/lib/useObservable'; -import { chartTooltip$, ChartTooltipState, ChartTooltipValue } from './chart_tooltip_service'; +import './_index.scss'; -type RefValue = HTMLElement | null; - -function useRefWithCallback(chartTooltipState?: ChartTooltipState) { - const ref = useRef(null); - - return (node: RefValue) => { - ref.current = node; - - if ( - node !== null && - node.parentElement !== null && - chartTooltipState !== undefined && - chartTooltipState.isTooltipVisible - ) { - const parentBounding = node.parentElement.getBoundingClientRect(); - - const { targetPosition, offset } = chartTooltipState; - - const contentWidth = document.body.clientWidth - parentBounding.left; - const tooltipWidth = node.clientWidth; - - let left = targetPosition.left + offset.x - parentBounding.left; - if (left + tooltipWidth > contentWidth) { - // the tooltip is hanging off the side of the page, - // so move it to the other side of the target - left = left - (tooltipWidth + offset.x); - } - - const top = targetPosition.top + offset.y - parentBounding.top; - - if ( - chartTooltipState.tooltipPosition.left !== left || - chartTooltipState.tooltipPosition.top !== top - ) { - // render the tooltip with adjusted position. - chartTooltip$.next({ - ...chartTooltipState, - tooltipPosition: { left, top }, - }); - } - } - }; -} +import { ChildrenArg, TooltipTriggerProps } from 'react-popper-tooltip/dist/types'; +import { ChartTooltipService, ChartTooltipValue, TooltipData } from './chart_tooltip_service'; const renderHeader = (headerData?: ChartTooltipValue, formatter?: TooltipValueFormatter) => { if (!headerData) { @@ -63,48 +22,101 @@ const renderHeader = (headerData?: ChartTooltipValue, formatter?: TooltipValueFo return formatter ? formatter(headerData) : headerData.label; }; -export const ChartTooltip: FC = () => { - const chartTooltipState = useObservable(chartTooltip$); - const chartTooltipElement = useRefWithCallback(chartTooltipState); +const Tooltip: FC<{ service: ChartTooltipService }> = React.memo(({ service }) => { + const [tooltipData, setData] = useState([]); + const refCallback = useRef(); - if (chartTooltipState === undefined || !chartTooltipState.isTooltipVisible) { - return

    ; - } + useEffect(() => { + const subscription = service.tooltipState$.subscribe(tooltipState => { + if (refCallback.current) { + // update trigger + refCallback.current(tooltipState.target); + } + setData(tooltipState.tooltipData); + }); + return () => { + subscription.unsubscribe(); + }; + }, []); + + const triggerCallback = useCallback( + (({ triggerRef }) => { + // obtain the reference to the trigger setter callback + // to update the target based on changes from the service. + refCallback.current = triggerRef; + // actual trigger is resolved by the service, hence don't render + return null; + }) as TooltipTriggerProps['children'], + [] + ); + + const tooltipCallback = useCallback( + (({ tooltipRef, getTooltipProps }) => { + return ( +
    + {tooltipData.length > 0 && tooltipData[0].skipHeader === undefined && ( +
    {renderHeader(tooltipData[0])}
    + )} + {tooltipData.length > 1 && ( +
    + {tooltipData + .slice(1) + .map(({ label, value, color, isHighlighted, seriesIdentifier, valueAccessor }) => { + const classes = classNames('mlChartTooltip__item', { + /* eslint @typescript-eslint/camelcase:0 */ + echTooltip__rowHighlighted: isHighlighted, + }); + return ( +
    + {label} + {value} +
    + ); + })} +
    + )} +
    + ); + }) as TooltipTriggerProps['tooltip'], + [tooltipData] + ); - const { tooltipData, tooltipHeaderFormatter, tooltipPosition } = chartTooltipState; - const transform = `translate(${tooltipPosition.left}px, ${tooltipPosition.top}px)`; + const isTooltipShown = tooltipData.length > 0; return ( -
    - {tooltipData.length > 0 && tooltipData[0].skipHeader === undefined && ( -
    - {renderHeader(tooltipData[0], tooltipHeaderFormatter)} -
    - )} - {tooltipData.length > 1 && ( -
    - {tooltipData - .slice(1) - .map(({ label, value, color, isHighlighted, seriesIdentifier, valueAccessor }) => { - const classes = classNames('mlChartTooltip__item', { - /* eslint @typescript-eslint/camelcase:0 */ - echTooltip__rowHighlighted: isHighlighted, - }); - return ( -
    - {label} - {value} -
    - ); - })} -
    - )} -
    + + {triggerCallback} + + ); +}); + +interface MlTooltipComponentProps { + children: (tooltipService: ChartTooltipService) => React.ReactElement; +} + +export const MlTooltipComponent: FC = ({ children }) => { + const service = useMemo(() => new ChartTooltipService(), []); + + return ( + <> + + {children(service)} + ); }; diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.d.ts b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.d.ts deleted file mode 100644 index e6b0b6b4270bd..0000000000000 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.d.ts +++ /dev/null @@ -1,42 +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 { BehaviorSubject } from 'rxjs'; - -import { TooltipValue, TooltipValueFormatter } from '@elastic/charts'; - -export declare const getChartTooltipDefaultState: () => ChartTooltipState; - -export interface ChartTooltipValue extends TooltipValue { - skipHeader?: boolean; -} - -interface ChartTooltipState { - isTooltipVisible: boolean; - offset: ToolTipOffset; - targetPosition: ClientRect; - tooltipData: ChartTooltipValue[]; - tooltipHeaderFormatter?: TooltipValueFormatter; - tooltipPosition: { left: number; top: number }; -} - -export declare const chartTooltip$: BehaviorSubject; - -interface ToolTipOffset { - x: number; - y: number; -} - -interface MlChartTooltipService { - show: ( - tooltipData: ChartTooltipValue[], - target?: HTMLElement | null, - offset?: ToolTipOffset - ) => void; - hide: () => void; -} - -export declare const mlChartTooltipService: MlChartTooltipService; diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.js b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.js deleted file mode 100644 index 59cf98e5ffd71..0000000000000 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.js +++ /dev/null @@ -1,37 +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 { BehaviorSubject } from 'rxjs'; - -export const getChartTooltipDefaultState = () => ({ - isTooltipVisible: false, - tooltipData: [], - offset: { x: 0, y: 0 }, - targetPosition: { left: 0, top: 0 }, - tooltipPosition: { left: 0, top: 0 }, -}); - -export const chartTooltip$ = new BehaviorSubject(getChartTooltipDefaultState()); - -export const mlChartTooltipService = { - show: (tooltipData, target, offset = { x: 0, y: 0 }) => { - if (typeof target !== 'undefined' && target !== null) { - chartTooltip$.next({ - ...chartTooltip$.getValue(), - isTooltipVisible: true, - offset, - targetPosition: target.getBoundingClientRect(), - tooltipData, - }); - } - }, - hide: () => { - chartTooltip$.next({ - ...getChartTooltipDefaultState(), - isTooltipVisible: false, - }); - }, -}; diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.test.ts b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.test.ts index aa1dbf92b0677..231854cd264c2 100644 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.test.ts +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.test.ts @@ -4,18 +4,61 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getChartTooltipDefaultState, mlChartTooltipService } from './chart_tooltip_service'; +import { + ChartTooltipService, + getChartTooltipDefaultState, + TooltipData, +} from './chart_tooltip_service'; -describe('ML - mlChartTooltipService', () => { - it('service API duck typing', () => { - expect(typeof mlChartTooltipService).toBe('object'); - expect(typeof mlChartTooltipService.show).toBe('function'); - expect(typeof mlChartTooltipService.hide).toBe('function'); +describe('ChartTooltipService', () => { + let service: ChartTooltipService; + + beforeEach(() => { + service = new ChartTooltipService(); + }); + + test('should update the tooltip state on show and hide', () => { + const spy = jest.fn(); + + service.tooltipState$.subscribe(spy); + + expect(spy).toHaveBeenCalledWith(getChartTooltipDefaultState()); + + const update = [ + { + label: 'new tooltip', + }, + ] as TooltipData; + const mockEl = document.createElement('div'); + + service.show(update, mockEl); + + expect(spy).toHaveBeenCalledWith({ + isTooltipVisible: true, + tooltipData: update, + offset: { x: 0, y: 0 }, + target: mockEl, + }); + + service.hide(); + + expect(spy).toHaveBeenCalledWith({ + isTooltipVisible: false, + tooltipData: ([] as unknown) as TooltipData, + offset: { x: 0, y: 0 }, + target: null, + }); }); - it('should fail silently when target is not defined', () => { - expect(() => { - mlChartTooltipService.show(getChartTooltipDefaultState().tooltipData, null); - }).not.toThrow('Call to show() should fail silently.'); + test('update the tooltip state only on a new value', () => { + const spy = jest.fn(); + + service.tooltipState$.subscribe(spy); + + expect(spy).toHaveBeenCalledWith(getChartTooltipDefaultState()); + + service.hide(); + + expect(spy).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.ts b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.ts new file mode 100644 index 0000000000000..b524e18102a95 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_service.ts @@ -0,0 +1,73 @@ +/* + * 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 { BehaviorSubject, Observable } from 'rxjs'; +import { isEqual } from 'lodash'; +import { TooltipValue, TooltipValueFormatter } from '@elastic/charts'; +import { distinctUntilChanged } from 'rxjs/operators'; + +export interface ChartTooltipValue extends TooltipValue { + skipHeader?: boolean; +} + +export interface TooltipHeader { + skipHeader: boolean; +} + +export type TooltipData = ChartTooltipValue[]; + +export interface ChartTooltipState { + isTooltipVisible: boolean; + offset: TooltipOffset; + tooltipData: TooltipData; + tooltipHeaderFormatter?: TooltipValueFormatter; + target: HTMLElement | null; +} + +interface TooltipOffset { + x: number; + y: number; +} + +export const getChartTooltipDefaultState = (): ChartTooltipState => ({ + isTooltipVisible: false, + tooltipData: ([] as unknown) as TooltipData, + offset: { x: 0, y: 0 }, + target: null, +}); + +export class ChartTooltipService { + private chartTooltip$ = new BehaviorSubject(getChartTooltipDefaultState()); + + public tooltipState$: Observable = this.chartTooltip$ + .asObservable() + .pipe(distinctUntilChanged(isEqual)); + + public show( + tooltipData: TooltipData, + target: HTMLElement, + offset: TooltipOffset = { x: 0, y: 0 } + ) { + if (!target) { + throw new Error('target is required for the tooltip positioning'); + } + + this.chartTooltip$.next({ + ...this.chartTooltip$.getValue(), + isTooltipVisible: true, + offset, + tooltipData, + target, + }); + } + + public hide() { + this.chartTooltip$.next({ + ...getChartTooltipDefaultState(), + isTooltipVisible: false, + }); + } +} diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/index.ts b/x-pack/plugins/ml/public/application/components/chart_tooltip/index.ts index 75c65ebaa0f50..ec19fe18bd324 100644 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/index.ts +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { mlChartTooltipService } from './chart_tooltip_service'; -export { ChartTooltip } from './chart_tooltip'; +export { ChartTooltipService } from './chart_tooltip_service'; +export { MlTooltipComponent } from './chart_tooltip'; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index 381e5e75356c1..f709c161bef17 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -4,45 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect, useRef, useCallback } from 'react'; -import PropTypes from 'prop-types'; - -import { - EuiButton, - EuiButtonEmpty, - EuiFlexItem, - EuiFlexGroup, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiSwitch, - EuiTitle, -} from '@elastic/eui'; +import React, { useState, useEffect } from 'react'; +import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useMlKibana } from '../../contexts/kibana'; import { Dictionary } from '../../../../common/types/common'; -import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; -import { ml } from '../../services/ml_api_service'; import { useUrlState } from '../../util/url_state'; // @ts-ignore -import { JobSelectorTable } from './job_selector_table/index'; -// @ts-ignore import { IdBadges } from './id_badges/index'; -// @ts-ignore -import { NewSelectionIdBadges } from './new_selection_id_badges/index'; -import { - getGroupsFromJobs, - getTimeRangeFromSelection, - normalizeTimes, -} from './job_select_service_utils'; +import { BADGE_LIMIT, JobSelectorFlyout, JobSelectorFlyoutProps } from './job_selector_flyout'; +import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; interface GroupObj { groupId: string; jobIds: string[]; } + function mergeSelection( jobIds: string[], groupObjs: GroupObj[], @@ -71,7 +49,7 @@ function mergeSelection( } type GroupsMap = Dictionary; -function getInitialGroupsMap(selectedGroups: GroupObj[]): GroupsMap { +export function getInitialGroupsMap(selectedGroups: GroupObj[]): GroupsMap { const map: GroupsMap = {}; if (selectedGroups.length) { @@ -83,81 +61,38 @@ function getInitialGroupsMap(selectedGroups: GroupObj[]): GroupsMap { return map; } -const BADGE_LIMIT = 10; -const DEFAULT_GANTT_BAR_WIDTH = 299; // pixels - interface JobSelectorProps { dateFormatTz: string; singleSelection: boolean; timeseriesOnly: boolean; } +export interface JobSelectionMaps { + jobsMap: Dictionary; + groupsMap: Dictionary; +} + export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: JobSelectorProps) { const [globalState, setGlobalState] = useUrlState('_g'); const selectedJobIds = globalState?.ml?.jobIds ?? []; const selectedGroups = globalState?.ml?.groups ?? []; - const [jobs, setJobs] = useState([]); - const [groups, setGroups] = useState([]); - const [maps, setMaps] = useState({ groupsMap: getInitialGroupsMap(selectedGroups), jobsMap: {} }); + const [maps, setMaps] = useState({ + groupsMap: getInitialGroupsMap(selectedGroups), + jobsMap: {}, + }); const [selectedIds, setSelectedIds] = useState( mergeSelection(selectedJobIds, selectedGroups, singleSelection) ); - const [newSelection, setNewSelection] = useState( - mergeSelection(selectedJobIds, selectedGroups, singleSelection) - ); - const [showAllBadges, setShowAllBadges] = useState(false); const [showAllBarBadges, setShowAllBarBadges] = useState(false); - const [applyTimeRange, setApplyTimeRange] = useState(true); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - const [ganttBarWidth, setGanttBarWidth] = useState(DEFAULT_GANTT_BAR_WIDTH); - const flyoutEl = useRef<{ flyout: HTMLElement }>(null); - const { - services: { notifications }, - } = useMlKibana(); // Ensure JobSelectionBar gets updated when selection via globalState changes. useEffect(() => { setSelectedIds(mergeSelection(selectedJobIds, selectedGroups, singleSelection)); }, [JSON.stringify([selectedJobIds, selectedGroups])]); - // Ensure current selected ids always show up in flyout - useEffect(() => { - setNewSelection(selectedIds); - }, [isFlyoutVisible]); // eslint-disable-line - - // Wrap handleResize in useCallback as it is a dependency for useEffect on line 131 below. - // Not wrapping it would cause this dependency to change on every render - const handleResize = useCallback(() => { - if (jobs.length > 0 && flyoutEl && flyoutEl.current && flyoutEl.current.flyout) { - // get all cols in flyout table - const tableHeaderCols: NodeListOf = flyoutEl.current.flyout.querySelectorAll( - 'table thead th' - ); - // get the width of the last col - const derivedWidth = tableHeaderCols[tableHeaderCols.length - 1].offsetWidth - 16; - const normalizedJobs = normalizeTimes(jobs, dateFormatTz, derivedWidth); - setJobs(normalizedJobs); - const { groups: updatedGroups } = getGroupsFromJobs(normalizedJobs); - setGroups(updatedGroups); - setGanttBarWidth(derivedWidth); - } - }, [dateFormatTz, jobs]); - - useEffect(() => { - // Ensure ganttBar width gets calculated on resize - window.addEventListener('resize', handleResize); - - return () => { - window.removeEventListener('resize', handleResize); - }; - }, [handleResize]); - - useEffect(() => { - handleResize(); - }, [handleResize, jobs]); - function closeFlyout() { setIsFlyoutVisible(false); } @@ -168,78 +103,26 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J function handleJobSelectionClick() { showFlyout(); - - ml.jobs - .jobsWithTimerange(dateFormatTz) - .then(resp => { - const normalizedJobs = normalizeTimes(resp.jobs, dateFormatTz, DEFAULT_GANTT_BAR_WIDTH); - const { groups: groupsWithTimerange, groupsMap } = getGroupsFromJobs(normalizedJobs); - setJobs(normalizedJobs); - setGroups(groupsWithTimerange); - setMaps({ groupsMap, jobsMap: resp.jobsMap }); - }) - .catch((err: any) => { - console.error('Error fetching jobs with time range', err); // eslint-disable-line - const { toasts } = notifications; - toasts.addDanger({ - title: i18n.translate('xpack.ml.jobSelector.jobFetchErrorMessage', { - defaultMessage: 'An error occurred fetching jobs. Refresh and try again.', - }), - }); - }); - } - - function handleNewSelection({ selectionFromTable }: { selectionFromTable: any }) { - setNewSelection(selectionFromTable); } - function applySelection() { - // allNewSelection will be a list of all job ids (including those from groups) selected from the table - const allNewSelection: string[] = []; - const groupSelection: Array<{ groupId: string; jobIds: string[] }> = []; - - newSelection.forEach(id => { - if (maps.groupsMap[id] !== undefined) { - // Push all jobs from selected groups into the newSelection list - allNewSelection.push(...maps.groupsMap[id]); - // if it's a group - push group obj to set in global state - groupSelection.push({ groupId: id, jobIds: maps.groupsMap[id] }); - } else { - allNewSelection.push(id); - } - }); - // create a Set to remove duplicate values - const allNewSelectionUnique = Array.from(new Set(allNewSelection)); - + const applySelection: JobSelectorFlyoutProps['onSelectionConfirmed'] = ({ + newSelection, + jobIds, + groups: newGroups, + time, + }) => { setSelectedIds(newSelection); - setNewSelection([]); - - closeFlyout(); - - const time = applyTimeRange - ? getTimeRangeFromSelection(jobs, allNewSelectionUnique) - : undefined; setGlobalState({ ml: { - jobIds: allNewSelectionUnique, - groups: groupSelection, + jobIds, + groups: newGroups, }, ...(time !== undefined ? { time } : {}), }); - } - - function toggleTimerangeSwitch() { - setApplyTimeRange(!applyTimeRange); - } - - function removeId(id: string) { - setNewSelection(newSelection.filter(item => item !== id)); - } - function clearSelection() { - setNewSelection([]); - } + closeFlyout(); + }; function renderJobSelectionBar() { return ( @@ -280,103 +163,16 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J function renderFlyout() { if (isFlyoutVisible) { return ( - - - -

    - {i18n.translate('xpack.ml.jobSelector.flyoutTitle', { - defaultMessage: 'Job selection', - })} -

    -
    -
    - - - - - setShowAllBadges(!showAllBadges)} - showAllBadges={showAllBadges} - /> - - - - - - {!singleSelection && newSelection.length > 0 && ( - - {i18n.translate('xpack.ml.jobSelector.clearAllFlyoutButton', { - defaultMessage: 'Clear all', - })} - - )} - - - - - - - - - - - - - - {i18n.translate('xpack.ml.jobSelector.applyFlyoutButton', { - defaultMessage: 'Apply', - })} - - - - - {i18n.translate('xpack.ml.jobSelector.closeFlyoutButton', { - defaultMessage: 'Close', - })} - - - - -
    + ); } } @@ -388,9 +184,3 @@ export function JobSelector({ dateFormatTz, singleSelection, timeseriesOnly }: J
    ); } - -JobSelector.propTypes = { - selectedJobIds: PropTypes.array, - singleSelection: PropTypes.bool, - timeseriesOnly: PropTypes.bool, -}; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/index.js b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/index.js rename to x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/index.ts diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.tsx similarity index 68% rename from x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js rename to x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.tsx index 4d2ab01e2a054..b2cae278c0e77 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.js +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_badge/job_selector_badge.tsx @@ -4,18 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { PropTypes } from 'prop-types'; -import { EuiBadge } from '@elastic/eui'; -import { tabColor } from '../../../../../common/util/group_color_utils'; +import React, { FC } from 'react'; +import { EuiBadge, EuiBadgeProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { tabColor } from '../../../../../common/util/group_color_utils'; -export function JobSelectorBadge({ icon, id, isGroup = false, numJobs, removeId }) { +interface JobSelectorBadgeProps { + icon?: boolean; + id: string; + isGroup?: boolean; + numJobs?: number; + removeId?: Function; +} + +export const JobSelectorBadge: FC = ({ + icon, + id, + isGroup = false, + numJobs, + removeId, +}) => { const color = isGroup ? tabColor(id) : 'hollow'; - let props = { color }; + let props = { color } as EuiBadgeProps; let jobCount; - if (icon === true) { + if (icon === true && removeId) { + // @ts-ignore props = { ...props, iconType: 'cross', @@ -37,11 +51,4 @@ export function JobSelectorBadge({ icon, id, isGroup = false, numJobs, removeId {`${id}${jobCount ? jobCount : ''}`} ); -} -JobSelectorBadge.propTypes = { - icon: PropTypes.bool, - id: PropTypes.string.isRequired, - isGroup: PropTypes.bool, - numJobs: PropTypes.number, - removeId: PropTypes.func, }; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx new file mode 100644 index 0000000000000..66aa05d2aaa97 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx @@ -0,0 +1,289 @@ +/* + * 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, useCallback, useEffect, useRef, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSwitch, + EuiTitle, +} from '@elastic/eui'; +import { NewSelectionIdBadges } from './new_selection_id_badges'; +// @ts-ignore +import { JobSelectorTable } from './job_selector_table'; +import { + getGroupsFromJobs, + getTimeRangeFromSelection, + normalizeTimes, +} from './job_select_service_utils'; +import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; +import { ml } from '../../services/ml_api_service'; +import { useMlKibana } from '../../contexts/kibana'; +import { JobSelectionMaps } from './job_selector'; + +export const BADGE_LIMIT = 10; +export const DEFAULT_GANTT_BAR_WIDTH = 299; // pixels + +export interface JobSelectorFlyoutProps { + dateFormatTz: string; + selectedIds?: string[]; + newSelection?: string[]; + onFlyoutClose: () => void; + onJobsFetched?: (maps: JobSelectionMaps) => void; + onSelectionChange?: (newSelection: string[]) => void; + onSelectionConfirmed: (payload: { + newSelection: string[]; + jobIds: string[]; + groups: Array<{ groupId: string; jobIds: string[] }>; + time: any; + }) => void; + singleSelection: boolean; + timeseriesOnly: boolean; + maps: JobSelectionMaps; + withTimeRangeSelector?: boolean; +} + +export const JobSelectorFlyout: FC = ({ + dateFormatTz, + selectedIds = [], + singleSelection, + timeseriesOnly, + onJobsFetched, + onSelectionChange, + onSelectionConfirmed, + onFlyoutClose, + maps, + withTimeRangeSelector = true, +}) => { + const { + services: { notifications }, + } = useMlKibana(); + + const [newSelection, setNewSelection] = useState(selectedIds); + + const [showAllBadges, setShowAllBadges] = useState(false); + const [applyTimeRange, setApplyTimeRange] = useState(true); + const [jobs, setJobs] = useState([]); + const [groups, setGroups] = useState([]); + const [ganttBarWidth, setGanttBarWidth] = useState(DEFAULT_GANTT_BAR_WIDTH); + const [jobGroupsMaps, setJobGroupsMaps] = useState(maps); + + const flyoutEl = useRef<{ flyout: HTMLElement }>(null); + + function applySelection() { + // allNewSelection will be a list of all job ids (including those from groups) selected from the table + const allNewSelection: string[] = []; + const groupSelection: Array<{ groupId: string; jobIds: string[] }> = []; + + newSelection.forEach(id => { + if (jobGroupsMaps.groupsMap[id] !== undefined) { + // Push all jobs from selected groups into the newSelection list + allNewSelection.push(...jobGroupsMaps.groupsMap[id]); + // if it's a group - push group obj to set in global state + groupSelection.push({ groupId: id, jobIds: jobGroupsMaps.groupsMap[id] }); + } else { + allNewSelection.push(id); + } + }); + // create a Set to remove duplicate values + const allNewSelectionUnique = Array.from(new Set(allNewSelection)); + + const time = applyTimeRange + ? getTimeRangeFromSelection(jobs, allNewSelectionUnique) + : undefined; + + onSelectionConfirmed({ + newSelection: allNewSelectionUnique, + jobIds: allNewSelectionUnique, + groups: groupSelection, + time, + }); + } + + function removeId(id: string) { + setNewSelection(newSelection.filter(item => item !== id)); + } + + function toggleTimerangeSwitch() { + setApplyTimeRange(!applyTimeRange); + } + + function clearSelection() { + setNewSelection([]); + } + + function handleNewSelection({ selectionFromTable }: { selectionFromTable: any }) { + setNewSelection(selectionFromTable); + } + + // Wrap handleResize in useCallback as it is a dependency for useEffect on line 131 below. + // Not wrapping it would cause this dependency to change on every render + const handleResize = useCallback(() => { + if (jobs.length > 0 && flyoutEl && flyoutEl.current && flyoutEl.current.flyout) { + // get all cols in flyout table + const tableHeaderCols: NodeListOf = flyoutEl.current.flyout.querySelectorAll( + 'table thead th' + ); + // get the width of the last col + const derivedWidth = tableHeaderCols[tableHeaderCols.length - 1].offsetWidth - 16; + const normalizedJobs = normalizeTimes(jobs, dateFormatTz, derivedWidth); + setJobs(normalizedJobs); + const { groups: updatedGroups } = getGroupsFromJobs(normalizedJobs); + setGroups(updatedGroups); + setGanttBarWidth(derivedWidth); + } + }, [dateFormatTz, jobs]); + + // Fetch jobs list on flyout open + useEffect(() => { + fetchJobs(); + }, []); + + async function fetchJobs() { + try { + const resp = await ml.jobs.jobsWithTimerange(dateFormatTz); + const normalizedJobs = normalizeTimes(resp.jobs, dateFormatTz, DEFAULT_GANTT_BAR_WIDTH); + const { groups: groupsWithTimerange, groupsMap } = getGroupsFromJobs(normalizedJobs); + setJobs(normalizedJobs); + setGroups(groupsWithTimerange); + setJobGroupsMaps({ groupsMap, jobsMap: resp.jobsMap }); + + if (onJobsFetched) { + onJobsFetched({ groupsMap, jobsMap: resp.jobsMap }); + } + } catch (e) { + console.error('Error fetching jobs with time range', e); // eslint-disable-line + const { toasts } = notifications; + toasts.addDanger({ + title: i18n.translate('xpack.ml.jobSelector.jobFetchErrorMessage', { + defaultMessage: 'An error occurred fetching jobs. Refresh and try again.', + }), + }); + } + } + + useEffect(() => { + // Ensure ganttBar width gets calculated on resize + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [handleResize]); + + useEffect(() => { + handleResize(); + }, [handleResize, jobs]); + + return ( + + + +

    + {i18n.translate('xpack.ml.jobSelector.flyoutTitle', { + defaultMessage: 'Job selection', + })} +

    +
    +
    + + + + + setShowAllBadges(!showAllBadges)} + showAllBadges={showAllBadges} + /> + + + + + + {!singleSelection && newSelection.length > 0 && ( + + {i18n.translate('xpack.ml.jobSelector.clearAllFlyoutButton', { + defaultMessage: 'Clear all', + })} + + )} + + {withTimeRangeSelector && ( + + + + )} + + + + + + + + + + {i18n.translate('xpack.ml.jobSelector.applyFlyoutButton', { + defaultMessage: 'Apply', + })} + + + + + {i18n.translate('xpack.ml.jobSelector.closeFlyoutButton', { + defaultMessage: 'Close', + })} + + + + +
    + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js index 64793d15f1e4a..c55e03776c09d 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_table/job_selector_table.js @@ -224,7 +224,7 @@ export function JobSelectorTable({ {jobs.length === 0 && } {jobs.length !== 0 && singleSelection === true && renderJobsTable()} - {jobs.length !== 0 && singleSelection === undefined && renderTabs()} + {jobs.length !== 0 && !singleSelection && renderTabs()} ); } diff --git a/x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/index.js b/x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/index.js rename to x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/index.ts diff --git a/x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.js b/x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.tsx similarity index 80% rename from x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.js rename to x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.tsx index 67dce47323889..4c018e72f3e10 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.js +++ b/x-pack/plugins/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.tsx @@ -4,20 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { PropTypes } from 'prop-types'; +import React, { FC, MouseEventHandler } from 'react'; import { EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; -import { JobSelectorBadge } from '../job_selector_badge'; import { i18n } from '@kbn/i18n'; +import { JobSelectorBadge } from '../job_selector_badge'; +import { JobSelectionMaps } from '../job_selector'; -export function NewSelectionIdBadges({ +interface NewSelectionIdBadgesProps { + limit: number; + maps: JobSelectionMaps; + newSelection: string[]; + onDeleteClick?: Function; + onLinkClick?: MouseEventHandler; + showAllBadges?: boolean; +} + +export const NewSelectionIdBadges: FC = ({ limit, maps, newSelection, onDeleteClick, onLinkClick, showAllBadges, -}) { +}) => { const badges = []; for (let i = 0; i < newSelection.length; i++) { @@ -60,16 +69,5 @@ export function NewSelectionIdBadges({ ); } - return badges; -} -NewSelectionIdBadges.propTypes = { - limit: PropTypes.number, - maps: PropTypes.shape({ - jobsMap: PropTypes.object, - groupsMap: PropTypes.object, - }), - newSelection: PropTypes.array, - onDeleteClick: PropTypes.func, - onLinkClick: PropTypes.func, - showAllBadges: PropTypes.bool, + return <>{badges}; }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 86ffc4a2614b9..06d89ab782167 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -41,7 +41,7 @@ import { useMlContext } from '../../contexts/ml'; import { kbnTypeToMLJobType } from '../../util/field_types_utils'; import { useTimefilter } from '../../contexts/kibana'; import { timeBasedIndexCheck, getQueryFromSavedSearch } from '../../util/index_utils'; -import { TimeBuckets } from '../../util/time_buckets'; +import { getTimeBucketsFromCache } from '../../util/time_buckets'; import { useUrlState } from '../../util/url_state'; import { FieldRequestConfig, FieldVisConfig } from './common'; import { ActionsPanel } from './components/actions_panel'; @@ -318,7 +318,7 @@ export const Page: FC = () => { // Obtain the interval to use for date histogram aggregations // (such as the document count chart). Aim for 75 bars. - const buckets = new TimeBuckets(); + const buckets = getTimeBucketsFromCache(); const tf = timefilter as any; let earliest: number | undefined; diff --git a/x-pack/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.js.snap b/x-pack/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.tsx.snap similarity index 100% rename from x-pack/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.js.snap rename to x-pack/plugins/ml/public/application/explorer/__snapshots__/explorer_swimlane.test.tsx.snap diff --git a/x-pack/plugins/ml/public/application/explorer/_explorer.scss b/x-pack/plugins/ml/public/application/explorer/_explorer.scss index 9fb2f0c3bed94..cfcba081983c2 100644 --- a/x-pack/plugins/ml/public/application/explorer/_explorer.scss +++ b/x-pack/plugins/ml/public/application/explorer/_explorer.scss @@ -106,164 +106,6 @@ padding: 0; margin-bottom: $euiSizeS; - div.ml-swimlanes { - margin: 0px 0px 0px 10px; - - div.cells-marker-container { - margin-left: 176px; - height: 22px; - white-space: nowrap; - - // background-color: #CCC; - .sl-cell { - height: 10px; - display: inline-block; - vertical-align: top; - margin-top: 16px; - text-align: center; - visibility: hidden; - cursor: default; - - i { - color: $euiColorDarkShade; - } - } - - .sl-cell-hover { - visibility: visible; - - i { - display: block; - margin-top: -6px; - } - } - - .sl-cell-active-hover { - visibility: visible; - - .floating-time-label { - display: inline-block; - } - } - } - - div.lane { - height: 30px; - border-bottom: 0px; - border-radius: 2px; - margin-top: -1px; - white-space: nowrap; - - div.lane-label { - display: inline-block; - font-size: 13px; - height: 30px; - text-align: right; - vertical-align: middle; - border-radius: 2px; - padding-right: 5px; - margin-right: 5px; - border: 1px solid transparent; - overflow: hidden; - text-overflow: ellipsis; - } - - div.lane-label.lane-label-masked { - opacity: 0.3; - } - - div.cells-container { - border: $euiBorderThin; - border-right: 0px; - display: inline-block; - height: 30px; - vertical-align: middle; - background-color: $euiColorEmptyShade; - - .sl-cell { - color: $euiColorEmptyShade; - cursor: default; - display: inline-block; - height: 29px; - border-right: $euiBorderThin; - vertical-align: top; - position: relative; - - .sl-cell-inner, - .sl-cell-inner-dragselect { - height: 26px; - margin: 1px; - border-radius: 2px; - text-align: center; - } - - .sl-cell-inner.sl-cell-inner-masked { - opacity: 0.2; - } - - .sl-cell-inner.sl-cell-inner-selected, - .sl-cell-inner-dragselect.sl-cell-inner-selected { - border: 2px solid $euiColorDarkShade; - } - - .sl-cell-inner.sl-cell-inner-selected.sl-cell-inner-masked, - .sl-cell-inner-dragselect.sl-cell-inner-selected.sl-cell-inner-masked { - border: 2px solid $euiColorFullShade; - opacity: 0.4; - } - } - - .sl-cell:hover { - .sl-cell-inner { - opacity: 0.8; - cursor: pointer; - } - } - - .sl-cell.ds-selected { - - .sl-cell-inner, - .sl-cell-inner-dragselect { - border: 2px solid $euiColorDarkShade; - border-radius: 2px; - opacity: 1; - } - } - - } - } - - div.lane:last-child { - div.cells-container { - .sl-cell { - border-bottom: $euiBorderThin; - } - } - } - - .time-tick-labels { - height: 25px; - margin-top: $euiSizeXS / 2; - margin-left: 175px; - - /* hide d3's domain line */ - path.domain { - display: none; - } - - /* hide d3's tick line */ - g.tick line { - display: none; - } - - /* override d3's default tick styles */ - g.tick text { - font-size: 11px; - fill: $euiColorMediumShade; - } - } - } - line.gridLine { stroke: $euiBorderColor; fill: none; @@ -328,3 +170,161 @@ } } } + +.ml-swimlanes { + margin: 0px 0px 0px 10px; + + div.cells-marker-container { + margin-left: 176px; + height: 22px; + white-space: nowrap; + + // background-color: #CCC; + .sl-cell { + height: 10px; + display: inline-block; + vertical-align: top; + margin-top: 16px; + text-align: center; + visibility: hidden; + cursor: default; + + i { + color: $euiColorDarkShade; + } + } + + .sl-cell-hover { + visibility: visible; + + i { + display: block; + margin-top: -6px; + } + } + + .sl-cell-active-hover { + visibility: visible; + + .floating-time-label { + display: inline-block; + } + } + } + + div.lane { + height: 30px; + border-bottom: 0px; + border-radius: 2px; + margin-top: -1px; + white-space: nowrap; + + div.lane-label { + display: inline-block; + font-size: 13px; + height: 30px; + text-align: right; + vertical-align: middle; + border-radius: 2px; + padding-right: 5px; + margin-right: 5px; + border: 1px solid transparent; + overflow: hidden; + text-overflow: ellipsis; + } + + div.lane-label.lane-label-masked { + opacity: 0.3; + } + + div.cells-container { + border: $euiBorderThin; + border-right: 0px; + display: inline-block; + height: 30px; + vertical-align: middle; + background-color: $euiColorEmptyShade; + + .sl-cell { + color: $euiColorEmptyShade; + cursor: default; + display: inline-block; + height: 29px; + border-right: $euiBorderThin; + vertical-align: top; + position: relative; + + .sl-cell-inner, + .sl-cell-inner-dragselect { + height: 26px; + margin: 1px; + border-radius: 2px; + text-align: center; + } + + .sl-cell-inner.sl-cell-inner-masked { + opacity: 0.2; + } + + .sl-cell-inner.sl-cell-inner-selected, + .sl-cell-inner-dragselect.sl-cell-inner-selected { + border: 2px solid $euiColorDarkShade; + } + + .sl-cell-inner.sl-cell-inner-selected.sl-cell-inner-masked, + .sl-cell-inner-dragselect.sl-cell-inner-selected.sl-cell-inner-masked { + border: 2px solid $euiColorFullShade; + opacity: 0.4; + } + } + + .sl-cell:hover { + .sl-cell-inner { + opacity: 0.8; + cursor: pointer; + } + } + + .sl-cell.ds-selected { + + .sl-cell-inner, + .sl-cell-inner-dragselect { + border: 2px solid $euiColorDarkShade; + border-radius: 2px; + opacity: 1; + } + } + + } + } + + div.lane:last-child { + div.cells-container { + .sl-cell { + border-bottom: $euiBorderThin; + } + } + } + + .time-tick-labels { + height: 25px; + margin-top: $euiSizeXS / 2; + margin-left: 175px; + + /* hide d3's domain line */ + path.domain { + display: none; + } + + /* hide d3's tick line */ + g.tick line { + display: none; + } + + /* override d3's default tick styles */ + g.tick text { + font-size: 11px; + fill: $euiColorMediumShade; + } + } +} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index d61d56d07b644..86d16776b68e2 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -36,9 +36,8 @@ import { ExplorerNoJobsFound, ExplorerNoResultsFound, } from './components'; -import { ChartTooltip } from '../components/chart_tooltip'; import { ExplorerSwimlane } from './explorer_swimlane'; -import { TimeBuckets } from '../util/time_buckets'; +import { getTimeBucketsFromCache } from '../util/time_buckets'; import { InfluencersList } from '../components/influencers_list'; import { ALLOW_CELL_RANGE_SELECTION, @@ -81,6 +80,7 @@ import { AnomaliesTable } from '../components/anomalies_table/anomalies_table'; import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public'; import { getTimefilter, getToastNotifications } from '../util/dependency_cache'; +import { MlTooltipComponent } from '../components/chart_tooltip'; function mapSwimlaneOptionsToEuiOptions(options) { return options.map(option => ({ @@ -179,6 +179,8 @@ export class Explorer extends React.Component { // Required to redraw the time series chart when the container is resized. this.resizeChecker = new ResizeChecker(this.resizeRef.current); this.resizeChecker.on('resize', this.resizeHandler); + + this.timeBuckets = getTimeBucketsFromCache(); } componentWillUnmount() { @@ -358,9 +360,6 @@ export class Explorer extends React.Component { return (
    - {/* Make sure ChartTooltip is inside wrapping div with 0px left/right padding so positioning can be inferred correctly. */} - - {noInfluencersConfigured === false && influencers !== undefined && (
    {showOverallSwimlane && ( - + + {tooltipService => ( + + )} + )}
    @@ -494,17 +498,22 @@ export class Explorer extends React.Component { onMouseLeave={this.onSwimlaneLeaveHandler} data-test-subj="mlAnomalyExplorerSwimlaneViewBy" > - + + {tooltipService => ( + + )} +
    )} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js index 5fc1160093a49..03426869b0ccf 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.js @@ -29,9 +29,8 @@ import { removeLabelOverlap, } from '../../util/chart_utils'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; -import { TimeBuckets } from '../../util/time_buckets'; +import { getTimeBucketsFromCache } from '../../util/time_buckets'; import { mlFieldFormatService } from '../../services/field_format_service'; -import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; import { CHART_TYPE } from '../explorer_constants'; @@ -50,6 +49,7 @@ export class ExplorerChartDistribution extends React.Component { static propTypes = { seriesConfig: PropTypes.object, severity: PropTypes.number, + tooltipService: PropTypes.object.isRequired, }; componentDidMount() { @@ -61,7 +61,7 @@ export class ExplorerChartDistribution extends React.Component { } renderChart() { - const { tooManyBuckets } = this.props; + const { tooManyBuckets, tooltipService } = this.props; const element = this.rootNode; const config = this.props.seriesConfig; @@ -259,7 +259,7 @@ export class ExplorerChartDistribution extends React.Component { function drawRareChartAxes() { // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); + const timeBuckets = getTimeBucketsFromCache(); const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; timeBuckets.setBounds(bounds); timeBuckets.setInterval('auto'); @@ -397,7 +397,7 @@ export class ExplorerChartDistribution extends React.Component { .on('mouseover', function(d) { showLineChartTooltip(d, this); }) - .on('mouseout', () => mlChartTooltipService.hide()); + .on('mouseout', () => tooltipService.hide()); // Update all dots to new positions. dots @@ -550,7 +550,7 @@ export class ExplorerChartDistribution extends React.Component { }); } - mlChartTooltipService.show(tooltipData, circle, { + tooltipService.show(tooltipData, circle, { x: LINE_CHART_ANOMALY_RADIUS * 3, y: LINE_CHART_ANOMALY_RADIUS * 2, }); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js index 71d777db5b2ec..06fd82204c1e1 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_distribution.test.js @@ -10,11 +10,13 @@ import seriesConfig from './__mocks__/mock_series_config_rare.json'; // Mock TimeBuckets and mlFieldFormatService, they don't play well // with the jest based test setup yet. jest.mock('../../util/time_buckets', () => ({ - TimeBuckets: function() { - this.setBounds = jest.fn(); - this.setInterval = jest.fn(); - this.getScaledDateFormat = jest.fn(); - }, + getTimeBucketsFromCache: jest.fn(() => { + return { + setBounds: jest.fn(), + setInterval: jest.fn(), + getScaledDateFormat: jest.fn(), + }; + }), })); jest.mock('../../services/field_format_service', () => ({ mlFieldFormatService: { @@ -43,8 +45,16 @@ describe('ExplorerChart', () => { afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox)); test('Initialize', () => { + const mockTooltipService = { + show: jest.fn(), + hide: jest.fn(), + }; + const wrapper = mountWithIntl( - + ); // without setting any attributes and corresponding data @@ -59,10 +69,16 @@ describe('ExplorerChart', () => { loading: true, }; + const mockTooltipService = { + show: jest.fn(), + hide: jest.fn(), + }; + const wrapper = mountWithIntl( ); @@ -83,12 +99,18 @@ describe('ExplorerChart', () => { chartLimits: chartLimits(chartData), }; + const mockTooltipService = { + show: jest.fn(), + hide: jest.fn(), + }; + // We create the element including a wrapper which sets the width: return mountWithIntl(
    ); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index dd9479be931a7..82041af39ca15 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -38,10 +38,9 @@ import { showMultiBucketAnomalyTooltip, } from '../../util/chart_utils'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; -import { TimeBuckets } from '../../util/time_buckets'; +import { getTimeBucketsFromCache } from '../../util/time_buckets'; import { mlEscape } from '../../util/string_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; -import { mlChartTooltipService } from '../../components/chart_tooltip/chart_tooltip_service'; import { i18n } from '@kbn/i18n'; @@ -53,6 +52,7 @@ export class ExplorerChartSingleMetric extends React.Component { tooManyBuckets: PropTypes.bool, seriesConfig: PropTypes.object, severity: PropTypes.number.isRequired, + tooltipService: PropTypes.object.isRequired, }; componentDidMount() { @@ -64,7 +64,7 @@ export class ExplorerChartSingleMetric extends React.Component { } renderChart() { - const { tooManyBuckets } = this.props; + const { tooManyBuckets, tooltipService } = this.props; const element = this.rootNode; const config = this.props.seriesConfig; @@ -191,7 +191,7 @@ export class ExplorerChartSingleMetric extends React.Component { function drawLineChartAxes() { // Get the scaled date format to use for x axis tick labels. - const timeBuckets = new TimeBuckets(); + const timeBuckets = getTimeBucketsFromCache(); const bounds = { min: moment(config.plotEarliest), max: moment(config.plotLatest) }; timeBuckets.setBounds(bounds); timeBuckets.setInterval('auto'); @@ -309,7 +309,7 @@ export class ExplorerChartSingleMetric extends React.Component { .on('mouseover', function(d) { showLineChartTooltip(d, this); }) - .on('mouseout', () => mlChartTooltipService.hide()); + .on('mouseout', () => tooltipService.hide()); const isAnomalyVisible = d => _.has(d, 'anomalyScore') && Number(d.anomalyScore) >= severity; @@ -354,7 +354,7 @@ export class ExplorerChartSingleMetric extends React.Component { .on('mouseover', function(d) { showLineChartTooltip(d, this); }) - .on('mouseout', () => mlChartTooltipService.hide()); + .on('mouseout', () => tooltipService.hide()); // Add rectangular markers for any scheduled events. const scheduledEventMarkers = lineChartGroup @@ -503,7 +503,7 @@ export class ExplorerChartSingleMetric extends React.Component { }); } - mlChartTooltipService.show(tooltipData, circle, { + tooltipService.show(tooltipData, circle, { x: LINE_CHART_ANOMALY_RADIUS * 3, y: LINE_CHART_ANOMALY_RADIUS * 2, }); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js index ca3e52308a936..54f541ceb7c3d 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.test.js @@ -10,11 +10,13 @@ import seriesConfig from './__mocks__/mock_series_config_filebeat.json'; // Mock TimeBuckets and mlFieldFormatService, they don't play well // with the jest based test setup yet. jest.mock('../../util/time_buckets', () => ({ - TimeBuckets: function() { - this.setBounds = jest.fn(); - this.setInterval = jest.fn(); - this.getScaledDateFormat = jest.fn(); - }, + getTimeBucketsFromCache: jest.fn(() => { + return { + setBounds: jest.fn(), + setInterval: jest.fn(), + getScaledDateFormat: jest.fn(), + }; + }), })); jest.mock('../../services/field_format_service', () => ({ mlFieldFormatService: { @@ -43,8 +45,16 @@ describe('ExplorerChart', () => { afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox)); test('Initialize', () => { + const mockTooltipService = { + show: jest.fn(), + hide: jest.fn(), + }; + const wrapper = mountWithIntl( - + ); // without setting any attributes and corresponding data @@ -59,10 +69,16 @@ describe('ExplorerChart', () => { loading: true, }; + const mockTooltipService = { + show: jest.fn(), + hide: jest.fn(), + }; + const wrapper = mountWithIntl( ); @@ -83,12 +99,18 @@ describe('ExplorerChart', () => { chartLimits: chartLimits(chartData), }; + const mockTooltipService = { + show: jest.fn(), + hide: jest.fn(), + }; + // We create the element including a wrapper which sets the width: return mountWithIntl(
    ); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 99de38c1e0a84..5b95931d31ab6 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import $ from 'jquery'; - import React from 'react'; import { @@ -29,6 +27,7 @@ import { ExplorerChartLabel } from './components/explorer_chart_label'; import { CHART_TYPE } from '../explorer_constants'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { MlTooltipComponent } from '../../components/chart_tooltip'; const textTooManyBuckets = i18n.translate('xpack.ml.explorer.charts.tooManyBucketsDescription', { defaultMessage: @@ -121,19 +120,29 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel }) chartType === CHART_TYPE.POPULATION_DISTRIBUTION ) { return ( - + + {tooltipService => ( + + )} + ); } return ( - + + {tooltipService => ( + + )} + ); })()} @@ -141,48 +150,36 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel }) } // Flex layout wrapper for all explorer charts -export class ExplorerChartsContainer extends React.Component { - componentDidMount() { - // Create a div for the tooltip. - $('.ml-explorer-charts-tooltip').remove(); - $('body').append( - '

    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≺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ƻgG#8PFd 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 z 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