From a806bac32dd46f3d9fcf4be4ac6e62932968fd50 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 18 Dec 2019 14:32:20 +0100 Subject: [PATCH 01/61] Fix ClusterManager call parameters (#53263) * Fix ClusterManager call parameters * remove static ClusterManager.create * migrate ClusterManager to ts * remove default import for ClusterManager and Worker --- .../{__mocks__/cluster.js => cluster.mock.ts} | 17 +++-- src/cli/cluster/cluster_manager.test.mocks.ts | 22 ++++++ ...anager.test.js => cluster_manager.test.ts} | 38 +++++----- ...{cluster_manager.js => cluster_manager.ts} | 65 ++++++++++------- .../{worker.test.js => worker.test.ts} | 51 +++++++------ src/cli/cluster/{worker.js => worker.ts} | 72 +++++++++++++------ src/core/server/legacy/legacy_service.test.ts | 28 +++++--- src/core/server/legacy/legacy_service.ts | 6 +- 8 files changed, 185 insertions(+), 114 deletions(-) rename src/cli/cluster/{__mocks__/cluster.js => cluster.mock.ts} (85%) create mode 100644 src/cli/cluster/cluster_manager.test.mocks.ts rename src/cli/cluster/{cluster_manager.test.js => cluster_manager.test.ts} (84%) rename src/cli/cluster/{cluster_manager.js => cluster_manager.ts} (83%) rename src/cli/cluster/{worker.test.js => worker.test.ts} (80%) rename src/cli/cluster/{worker.js => worker.ts} (75%) diff --git a/src/cli/cluster/__mocks__/cluster.js b/src/cli/cluster/cluster.mock.ts similarity index 85% rename from src/cli/cluster/__mocks__/cluster.js rename to src/cli/cluster/cluster.mock.ts index d653771136ae6..332f8aad53ba1 100644 --- a/src/cli/cluster/__mocks__/cluster.js +++ b/src/cli/cluster/cluster.mock.ts @@ -18,12 +18,15 @@ */ /* eslint-env jest */ +// eslint-disable-next-line max-classes-per-file import EventEmitter from 'events'; import { assign, random } from 'lodash'; import { delay } from 'bluebird'; class MockClusterFork extends EventEmitter { - constructor(cluster) { + public exitCode = 0; + + constructor(cluster: MockCluster) { super(); let dead = true; @@ -49,9 +52,9 @@ class MockClusterFork extends EventEmitter { send: jest.fn(), }); - jest.spyOn(this, 'on'); - jest.spyOn(this, 'off'); - jest.spyOn(this, 'emit'); + jest.spyOn(this as EventEmitter, 'on'); + jest.spyOn(this as EventEmitter, 'off'); + jest.spyOn(this as EventEmitter, 'emit'); (async () => { await wait(); @@ -61,11 +64,7 @@ class MockClusterFork extends EventEmitter { } } -class MockCluster extends EventEmitter { +export class MockCluster extends EventEmitter { fork = jest.fn(() => new MockClusterFork(this)); setupMaster = jest.fn(); } - -export function mockCluster() { - return new MockCluster(); -} diff --git a/src/cli/cluster/cluster_manager.test.mocks.ts b/src/cli/cluster/cluster_manager.test.mocks.ts new file mode 100644 index 0000000000000..53984fd12cbf1 --- /dev/null +++ b/src/cli/cluster/cluster_manager.test.mocks.ts @@ -0,0 +1,22 @@ +/* + * 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 { MockCluster } from './cluster.mock'; +export const mockCluster = new MockCluster(); +jest.mock('cluster', () => mockCluster); diff --git a/src/cli/cluster/cluster_manager.test.js b/src/cli/cluster/cluster_manager.test.ts similarity index 84% rename from src/cli/cluster/cluster_manager.test.js rename to src/cli/cluster/cluster_manager.test.ts index be8a096db9a66..bd37e854e1691 100644 --- a/src/cli/cluster/cluster_manager.test.js +++ b/src/cli/cluster/cluster_manager.test.ts @@ -17,8 +17,7 @@ * under the License. */ -import { mockCluster } from './__mocks__/cluster'; -jest.mock('cluster', () => mockCluster()); +import { mockCluster } from './cluster_manager.test.mocks'; jest.mock('readline', () => ({ createInterface: jest.fn(() => ({ on: jest.fn(), @@ -27,15 +26,14 @@ jest.mock('readline', () => ({ })), })); -import cluster from 'cluster'; import { sample } from 'lodash'; -import ClusterManager from './cluster_manager'; -import Worker from './worker'; +import { ClusterManager } from './cluster_manager'; +import { Worker } from './worker'; describe('CLI cluster manager', () => { beforeEach(() => { - cluster.fork.mockImplementation(() => { + mockCluster.fork.mockImplementation(() => { return { process: { kill: jest.fn(), @@ -44,16 +42,16 @@ describe('CLI cluster manager', () => { off: jest.fn(), on: jest.fn(), send: jest.fn(), - }; + } as any; }); }); afterEach(() => { - cluster.fork.mockReset(); + mockCluster.fork.mockReset(); }); test('has two workers', () => { - const manager = ClusterManager.create({}); + const manager = new ClusterManager({}, {} as any); expect(manager.workers).toHaveLength(2); for (const worker of manager.workers) expect(worker).toBeInstanceOf(Worker); @@ -63,7 +61,7 @@ describe('CLI cluster manager', () => { }); test('delivers broadcast messages to other workers', () => { - const manager = ClusterManager.create({}); + const manager = new ClusterManager({}, {} as any); for (const worker of manager.workers) { Worker.prototype.start.call(worker); // bypass the debounced start method @@ -76,10 +74,10 @@ describe('CLI cluster manager', () => { messenger.emit('broadcast', football); for (const worker of manager.workers) { if (worker === messenger) { - expect(worker.fork.send).not.toHaveBeenCalled(); + expect(worker.fork!.send).not.toHaveBeenCalled(); } else { - expect(worker.fork.send).toHaveBeenCalledTimes(1); - expect(worker.fork.send).toHaveBeenCalledWith(football); + expect(worker.fork!.send).toHaveBeenCalledTimes(1); + expect(worker.fork!.send).toHaveBeenCalledWith(football); } } }); @@ -88,7 +86,7 @@ describe('CLI cluster manager', () => { test('correctly configures `BasePathProxy`.', async () => { const basePathProxyMock = { start: jest.fn() }; - ClusterManager.create({}, {}, basePathProxyMock); + new ClusterManager({}, {} as any, basePathProxyMock as any); expect(basePathProxyMock.start).toHaveBeenCalledWith({ shouldRedirectFromOldBasePath: expect.any(Function), @@ -97,13 +95,13 @@ describe('CLI cluster manager', () => { }); describe('proxy is configured with the correct `shouldRedirectFromOldBasePath` and `blockUntil` functions.', () => { - let clusterManager; - let shouldRedirectFromOldBasePath; - let blockUntil; + let clusterManager: ClusterManager; + let shouldRedirectFromOldBasePath: (path: string) => boolean; + let blockUntil: () => Promise; beforeEach(async () => { const basePathProxyMock = { start: jest.fn() }; - clusterManager = ClusterManager.create({}, {}, basePathProxyMock); + clusterManager = new ClusterManager({}, {} as any, basePathProxyMock as any); jest.spyOn(clusterManager.server, 'on'); jest.spyOn(clusterManager.server, 'off'); @@ -146,7 +144,7 @@ describe('CLI cluster manager', () => { expect(clusterManager.server.on).toHaveBeenCalledTimes(2); expect(clusterManager.server.on).toHaveBeenCalledWith('crashed', expect.any(Function)); - const [, [eventName, onCrashed]] = clusterManager.server.on.mock.calls; + const [, [eventName, onCrashed]] = (clusterManager.server.on as jest.Mock).mock.calls; // Check event name to make sure we call the right callback, // in Jest 23 we could use `toHaveBeenNthCalledWith` instead. expect(eventName).toBe('crashed'); @@ -164,7 +162,7 @@ describe('CLI cluster manager', () => { expect(clusterManager.server.on).toHaveBeenCalledTimes(2); expect(clusterManager.server.on).toHaveBeenCalledWith('listening', expect.any(Function)); - const [[eventName, onListening]] = clusterManager.server.on.mock.calls; + const [[eventName, onListening]] = (clusterManager.server.on as jest.Mock).mock.calls; // Check event name to make sure we call the right callback, // in Jest 23 we could use `toHaveBeenNthCalledWith` instead. expect(eventName).toBe('listening'); diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.ts similarity index 83% rename from src/cli/cluster/cluster_manager.js rename to src/cli/cluster/cluster_manager.ts index cd1b3a0dadfc6..d97f7485fb4d2 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.ts @@ -20,26 +20,38 @@ import { resolve } from 'path'; import { format as formatUrl } from 'url'; import opn from 'opn'; - import { debounce, invoke, bindAll, once, uniq } from 'lodash'; import * as Rx from 'rxjs'; import { first, mapTo, filter, map, take } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/dev-utils'; +import { FSWatcher } from 'chokidar'; + +import { LegacyConfig } from '../../core/server/legacy/config'; +import { BasePathProxyServer } from '../../core/server/http'; +// @ts-ignore import Log from '../log'; -import Worker from './worker'; -import { Config } from '../../legacy/server/config/config'; +import { Worker } from './worker'; process.env.kbnWorkerType = 'managr'; -export default class ClusterManager { - static create(opts, settings = {}, basePathProxy) { - return new ClusterManager(opts, Config.withDefaultSchema(settings), basePathProxy); - } - - constructor(opts, config, basePathProxy) { +export class ClusterManager { + public optimizer: Worker; + public server: Worker; + public workers: Worker[]; + + private watcher: FSWatcher | null = null; + private basePathProxy: BasePathProxyServer | undefined; + private log: any; + private addedCount = 0; + private inReplMode: boolean; + + constructor( + opts: Record, + config: LegacyConfig, + basePathProxy?: BasePathProxyServer + ) { this.log = new Log(opts.quiet, opts.silent); - this.addedCount = 0; this.inReplMode = !!opts.repl; this.basePathProxy = basePathProxy; @@ -79,7 +91,7 @@ export default class ClusterManager { worker.on('broadcast', msg => { this.workers.forEach(to => { if (to !== worker && to.online) { - to.fork.send(msg); + to.fork!.send(msg); } }); }); @@ -90,10 +102,10 @@ export default class ClusterManager { // and all workers. This is only used by LogRotator service // when the cluster mode is enabled this.server.on('reloadLoggingConfigFromServerWorker', () => { - process.emit('message', { reloadLoggingConfig: true }); + process.emit('message' as any, { reloadLoggingConfig: true } as any); this.workers.forEach(worker => { - worker.fork.send({ reloadLoggingConfig: true }); + worker.fork!.send({ reloadLoggingConfig: true }); }); }); @@ -111,9 +123,9 @@ export default class ClusterManager { } if (opts.watch) { - const pluginPaths = config.get('plugins.paths'); + const pluginPaths = config.get('plugins.paths'); const scanDirs = [ - ...config.get('plugins.scanDirs'), + ...config.get('plugins.scanDirs'), resolve(REPO_ROOT, 'src/plugins'), resolve(REPO_ROOT, 'x-pack/plugins'), ]; @@ -131,7 +143,7 @@ export default class ClusterManager { resolve(path, 'scripts'), resolve(path, 'docs') ), - [] + [] as string[] ); this.setupWatching(extraPaths, pluginInternalDirsIgnore); @@ -149,7 +161,7 @@ export default class ClusterManager { } } - setupOpen(openUrl) { + setupOpen(openUrl: string) { const serverListening$ = Rx.merge( Rx.fromEvent(this.server, 'listening').pipe(mapTo(true)), Rx.fromEvent(this.server, 'fork:exit').pipe(mapTo(false)), @@ -157,7 +169,7 @@ export default class ClusterManager { ); const optimizeSuccess$ = Rx.fromEvent(this.optimizer, 'optimizeStatus').pipe( - map(msg => !!msg.success) + map((msg: any) => !!msg.success) ); Rx.combineLatest(serverListening$, optimizeSuccess$) @@ -169,8 +181,10 @@ export default class ClusterManager { .then(() => opn(openUrl)); } - setupWatching(extraPaths, pluginInternalDirsIgnore) { + setupWatching(extraPaths: string[], pluginInternalDirsIgnore: string[]) { + // eslint-disable-next-line @typescript-eslint/no-var-requires const chokidar = require('chokidar'); + // eslint-disable-next-line @typescript-eslint/no-var-requires const { fromRoot } = require('../../core/server/utils'); const watchPaths = [ @@ -204,7 +218,7 @@ export default class ClusterManager { ...ignorePaths, 'plugins/java_languageserver', ], - }); + }) as FSWatcher; this.watcher.on('add', this.onWatcherAdd); this.watcher.on('error', this.onWatcherError); @@ -213,8 +227,8 @@ export default class ClusterManager { 'ready', once(() => { // start sending changes to workers - this.watcher.removeListener('add', this.onWatcherAdd); - this.watcher.on('all', this.onWatcherChange); + this.watcher!.removeListener('add', this.onWatcherAdd); + this.watcher!.on('all', this.onWatcherChange); this.log.good('watching for changes', `(${this.addedCount} files)`); this.startCluster(); @@ -229,6 +243,7 @@ export default class ClusterManager { if (this.inReplMode) { return; } + // eslint-disable-next-line @typescript-eslint/no-var-requires const readline = require('readline'); const rl = readline.createInterface(process.stdin, process.stdout); @@ -263,16 +278,16 @@ export default class ClusterManager { this.addedCount += 1; } - onWatcherChange(e, path) { + onWatcherChange(e: any, path: string) { invoke(this.workers, 'onChange', path); } - onWatcherError(err) { + onWatcherError(err: any) { this.log.bad('failed to watch files!\n', err.stack); process.exit(1); // eslint-disable-line no-process-exit } - shouldRedirectFromOldBasePath(path) { + shouldRedirectFromOldBasePath(path: string) { // strip `s/{id}` prefix when checking for need to redirect if (path.startsWith('s/')) { path = path diff --git a/src/cli/cluster/worker.test.js b/src/cli/cluster/worker.test.ts similarity index 80% rename from src/cli/cluster/worker.test.js rename to src/cli/cluster/worker.test.ts index b43cc123abcbb..4f9337681e083 100644 --- a/src/cli/cluster/worker.test.js +++ b/src/cli/cluster/worker.test.ts @@ -17,22 +17,20 @@ * under the License. */ -import { mockCluster } from './__mocks__/cluster'; -jest.mock('cluster', () => mockCluster()); +import { mockCluster } from './cluster_manager.test.mocks'; -import cluster from 'cluster'; - -import Worker from './worker'; +import { Worker, ClusterWorker } from './worker'; +// @ts-ignore import Log from '../log'; -const workersToShutdown = []; +const workersToShutdown: Worker[] = []; -function assertListenerAdded(emitter, event) { +function assertListenerAdded(emitter: NodeJS.EventEmitter, event: any) { expect(emitter.on).toHaveBeenCalledWith(event, expect.any(Function)); } -function assertListenerRemoved(emitter, event) { - const [, onEventListener] = emitter.on.mock.calls.find(([eventName]) => { +function assertListenerRemoved(emitter: NodeJS.EventEmitter, event: any) { + const [, onEventListener] = (emitter.on as jest.Mock).mock.calls.find(([eventName]) => { return eventName === event; }); @@ -44,6 +42,7 @@ function setup(opts = {}) { log: new Log(false, true), ...opts, baseArgv: [], + type: 'test', }); workersToShutdown.push(worker); @@ -53,7 +52,7 @@ function setup(opts = {}) { describe('CLI cluster manager', () => { afterEach(async () => { while (workersToShutdown.length > 0) { - const worker = workersToShutdown.pop(); + const worker = workersToShutdown.pop() as Worker; // If `fork` exists we should set `exitCode` to the non-zero value to // prevent worker from auto restart. if (worker.fork) { @@ -63,14 +62,14 @@ describe('CLI cluster manager', () => { await worker.shutdown(); } - cluster.fork.mockClear(); + mockCluster.fork.mockClear(); }); describe('#onChange', () => { describe('opts.watch = true', () => { test('restarts the fork', () => { const worker = setup({ watch: true }); - jest.spyOn(worker, 'start').mockImplementation(() => {}); + jest.spyOn(worker, 'start').mockResolvedValue(); worker.onChange('/some/path'); expect(worker.changes).toEqual(['/some/path']); expect(worker.start).toHaveBeenCalledTimes(1); @@ -80,7 +79,7 @@ describe('CLI cluster manager', () => { describe('opts.watch = false', () => { test('does not restart the fork', () => { const worker = setup({ watch: false }); - jest.spyOn(worker, 'start').mockImplementation(() => {}); + jest.spyOn(worker, 'start').mockResolvedValue(); worker.onChange('/some/path'); expect(worker.changes).toEqual([]); expect(worker.start).not.toHaveBeenCalled(); @@ -94,13 +93,13 @@ describe('CLI cluster manager', () => { const worker = setup(); await worker.start(); expect(worker).toHaveProperty('online', true); - const fork = worker.fork; - expect(fork.process.kill).not.toHaveBeenCalled(); + const fork = worker.fork as ClusterWorker; + expect(fork!.process.kill).not.toHaveBeenCalled(); assertListenerAdded(fork, 'message'); assertListenerAdded(fork, 'online'); assertListenerAdded(fork, 'disconnect'); await worker.shutdown(); - expect(fork.process.kill).toHaveBeenCalledTimes(1); + expect(fork!.process.kill).toHaveBeenCalledTimes(1); assertListenerRemoved(fork, 'message'); assertListenerRemoved(fork, 'online'); assertListenerRemoved(fork, 'disconnect'); @@ -120,7 +119,7 @@ describe('CLI cluster manager', () => { test(`is bound to fork's message event`, async () => { const worker = setup(); await worker.start(); - expect(worker.fork.on).toHaveBeenCalledWith('message', expect.any(Function)); + expect(worker.fork!.on).toHaveBeenCalledWith('message', expect.any(Function)); }); }); @@ -138,8 +137,8 @@ describe('CLI cluster manager', () => { test('calls #onMessage with message parts', () => { const worker = setup(); jest.spyOn(worker, 'onMessage').mockImplementation(() => {}); - worker.parseIncomingMessage([10, 100, 1000, 10000]); - expect(worker.onMessage).toHaveBeenCalledWith(10, 100, 1000, 10000); + worker.parseIncomingMessage(['event', 'some-data']); + expect(worker.onMessage).toHaveBeenCalledWith('event', 'some-data'); }); }); }); @@ -149,7 +148,7 @@ describe('CLI cluster manager', () => { test('emits the data to be broadcasted', () => { const worker = setup(); const data = {}; - jest.spyOn(worker, 'emit').mockImplementation(() => {}); + jest.spyOn(worker, 'emit').mockImplementation(() => true); worker.onMessage('WORKER_BROADCAST', data); expect(worker.emit).toHaveBeenCalledWith('broadcast', data); }); @@ -158,7 +157,7 @@ describe('CLI cluster manager', () => { describe('when sent WORKER_LISTENING message', () => { test('sets the listening flag and emits the listening event', () => { const worker = setup(); - jest.spyOn(worker, 'emit').mockImplementation(() => {}); + jest.spyOn(worker, 'emit').mockImplementation(() => true); expect(worker).toHaveProperty('listening', false); worker.onMessage('WORKER_LISTENING'); expect(worker).toHaveProperty('listening', true); @@ -170,8 +169,6 @@ describe('CLI cluster manager', () => { test('does nothing', () => { const worker = setup(); worker.onMessage('asdlfkajsdfahsdfiohuasdofihsdoif'); - worker.onMessage({}); - worker.onMessage(23049283094); }); }); }); @@ -185,7 +182,7 @@ describe('CLI cluster manager', () => { await worker.start(); - expect(cluster.fork).toHaveBeenCalledTimes(1); + expect(mockCluster.fork).toHaveBeenCalledTimes(1); expect(worker.on).toHaveBeenCalledWith('fork:online', expect.any(Function)); }); @@ -193,12 +190,12 @@ describe('CLI cluster manager', () => { const worker = setup(); jest.spyOn(process, 'on'); - jest.spyOn(cluster, 'on'); + jest.spyOn(mockCluster, 'on'); await worker.start(); - expect(cluster.on).toHaveBeenCalledTimes(1); - expect(cluster.on).toHaveBeenCalledWith('exit', expect.any(Function)); + expect(mockCluster.on).toHaveBeenCalledTimes(1); + expect(mockCluster.on).toHaveBeenCalledWith('exit', expect.any(Function)); expect(process.on).toHaveBeenCalledTimes(1); expect(process.on).toHaveBeenCalledWith('exit', expect.any(Function)); }); diff --git a/src/cli/cluster/worker.js b/src/cli/cluster/worker.ts similarity index 75% rename from src/cli/cluster/worker.js rename to src/cli/cluster/worker.ts index 2250075f20a60..fb87f1a87654c 100644 --- a/src/cli/cluster/worker.js +++ b/src/cli/cluster/worker.ts @@ -21,25 +21,57 @@ import _ from 'lodash'; import cluster from 'cluster'; import { EventEmitter } from 'events'; -import { BinderFor } from '../../legacy/utils'; +import { BinderFor } from '../../legacy/utils/binder_for'; import { fromRoot } from '../../core/server/utils'; const cliPath = fromRoot('src/cli'); const baseArgs = _.difference(process.argv.slice(2), ['--no-watch']); const baseArgv = [process.execPath, cliPath].concat(baseArgs); +export type ClusterWorker = cluster.Worker & { + killed: boolean; + exitCode?: number; +}; + cluster.setupMaster({ exec: cliPath, silent: false, }); -const dead = fork => { +const dead = (fork: ClusterWorker) => { return fork.isDead() || fork.killed; }; -export default class Worker extends EventEmitter { - constructor(opts) { - opts = opts || {}; +interface WorkerOptions { + type: string; + log: any; // src/cli/log.js + argv?: string[]; + title?: string; + watch?: boolean; + baseArgv?: string[]; +} + +export class Worker extends EventEmitter { + private readonly clusterBinder: BinderFor; + private readonly processBinder: BinderFor; + + private type: string; + private title: string; + private log: any; + private forkBinder: BinderFor | null = null; + private startCount: number; + private watch: boolean; + private env: Record; + + public fork: ClusterWorker | null = null; + public changes: string[]; + + // status flags + public online = false; // the fork can accept messages + public listening = false; // the fork is listening for connections + public crashed = false; // the fork crashed + + constructor(opts: WorkerOptions) { super(); this.log = opts.log; @@ -48,15 +80,9 @@ export default class Worker extends EventEmitter { this.watch = opts.watch !== false; this.startCount = 0; - // status flags - this.online = false; // the fork can accept messages - this.listening = false; // the fork is listening for connections - this.crashed = false; // the fork crashed - this.changes = []; - this.forkBinder = null; // defined when the fork is - this.clusterBinder = new BinderFor(cluster); + this.clusterBinder = new BinderFor(cluster as any); // lack the 'off' method this.processBinder = new BinderFor(process); this.env = { @@ -66,7 +92,7 @@ export default class Worker extends EventEmitter { }; } - onExit(fork, code) { + onExit(fork: ClusterWorker, code: number) { if (this.fork !== fork) return; // we have our fork's exit, so stop listening for others @@ -91,7 +117,7 @@ export default class Worker extends EventEmitter { } } - onChange(path) { + onChange(path: string) { if (!this.watch) return; this.changes.push(path); this.start(); @@ -104,7 +130,7 @@ export default class Worker extends EventEmitter { this.fork.killed = true; // stop listening to the fork, it's just going to die - this.forkBinder.destroy(); + this.forkBinder!.destroy(); // we don't need to react to process.exit anymore this.processBinder.destroy(); @@ -114,12 +140,14 @@ export default class Worker extends EventEmitter { } } - parseIncomingMessage(msg) { - if (!Array.isArray(msg)) return; - this.onMessage(...msg); + parseIncomingMessage(msg: any) { + if (!Array.isArray(msg)) { + return; + } + this.onMessage(msg[0], msg[1]); } - onMessage(type, data) { + onMessage(type: string, data?: any) { switch (type) { case 'WORKER_BROADCAST': this.emit('broadcast', data); @@ -170,16 +198,16 @@ export default class Worker extends EventEmitter { this.log.warn(`restarting ${this.title}...`); } - this.fork = cluster.fork(this.env); + this.fork = cluster.fork(this.env) as ClusterWorker; this.forkBinder = new BinderFor(this.fork); // when the fork sends a message, comes online, or loses its connection, then react - this.forkBinder.on('message', msg => this.parseIncomingMessage(msg)); + this.forkBinder.on('message', (msg: any) => this.parseIncomingMessage(msg)); this.forkBinder.on('online', () => this.onOnline()); this.forkBinder.on('disconnect', () => this.onDisconnect()); // when the cluster says a fork has exited, check if it is ours - this.clusterBinder.on('exit', (fork, code) => this.onExit(fork, code)); + this.clusterBinder.on('exit', (fork: ClusterWorker, code: number) => this.onExit(fork, code)); // when the process exits, make sure we kill our workers this.processBinder.on('exit', () => this.shutdown()); diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 17ec1e9756432..7025e96d9ecb4 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -27,7 +27,7 @@ import { findLegacyPluginSpecsMock } from './legacy_service.test.mocks'; import { BehaviorSubject, throwError } from 'rxjs'; import { LegacyService, LegacyServiceSetupDeps, LegacyServiceStartDeps } from '.'; // @ts-ignore: implicit any for JS file -import MockClusterManager from '../../../cli/cluster/cluster_manager'; +import { ClusterManager as MockClusterManager } from '../../../cli/cluster/cluster_manager'; import KbnServer from '../../../legacy/server/kbn_server'; import { Config, Env, ObjectToConfigAdapter } from '../config'; import { getEnvOptions } from '../config/__mocks__/env'; @@ -354,9 +354,15 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start(startDeps); - const [[cliArgs, , basePathProxy]] = MockClusterManager.create.mock.calls; - expect(cliArgs.basePath).toBe(false); - expect(basePathProxy).not.toBeDefined(); + expect(MockClusterManager).toHaveBeenCalledTimes(1); + expect(MockClusterManager).toHaveBeenCalledWith( + expect.objectContaining({ silent: true, basePath: false }), + expect.objectContaining({ + get: expect.any(Function), + set: expect.any(Function), + }), + undefined + ); }); test('creates ClusterManager with base path proxy.', async () => { @@ -376,11 +382,15 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start(startDeps); - expect(MockClusterManager.create).toBeCalledTimes(1); - - const [[cliArgs, , basePathProxy]] = MockClusterManager.create.mock.calls; - expect(cliArgs.basePath).toEqual(true); - expect(basePathProxy).toBeInstanceOf(BasePathProxyServer); + expect(MockClusterManager).toHaveBeenCalledTimes(1); + expect(MockClusterManager).toHaveBeenCalledWith( + expect.objectContaining({ quiet: true, basePath: true }), + expect.objectContaining({ + get: expect.any(Function), + set: expect.any(Function), + }), + expect.any(BasePathProxyServer) + ); }); }); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 1bba38433d7f4..412f4570887a4 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -244,7 +244,7 @@ export class LegacyService implements CoreService { private async createClusterManager(config: LegacyConfig) { const basePathProxy$ = this.coreContext.env.cliArgs.basePath - ? combineLatest(this.devConfig$, this.httpConfig$).pipe( + ? combineLatest([this.devConfig$, this.httpConfig$]).pipe( first(), map( ([dev, http]) => @@ -253,7 +253,9 @@ export class LegacyService implements CoreService { ) : EMPTY; - require('../../../cli/cluster/cluster_manager').create( + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ClusterManager } = require('../../../cli/cluster/cluster_manager'); + return new ClusterManager( this.coreContext.env.cliArgs, config, await basePathProxy$.toPromise() From 42d868db7f7116b3fcd073eb6a426b0573ad9ce5 Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Wed, 18 Dec 2019 14:52:18 +0100 Subject: [PATCH 02/61] [RFC][skip-ci] Prevent plugins from blocking Kibana startup (#45796) * Draft RFC for unblocking plugin lifecycle methods * Draft RFC for unblocking kibana startup * Rename rfc from 0006 to 0007 * Add references to TC39 top-level await * Update with review suggestion Co-Authored-By: Court Ewing * Update RFC#0007 * Apply suggestions from code review Co-Authored-By: Aleh Zasypkin * Address review comments from @joshdover and @azasypkin 1. Fleshed out motivation, this RFC doesn't prevent errors, but isolates the impact on the rest of Kibana. 2. Added a footnote explaining that sync lifecycles can still block on sync for loops, so it's not a perfect guarantee (from @azasypkin). 3. Updated IContextProvider type signature in (2) to match latest master 4. Dynamically reloading configuration changes should be limited to a whitelist, risky changes like the Elasticsearch host should still require a complete restart. Added to (3) based on https://github.com/elastic/kibana/pull/45796#discussion_r331277153 5. Added Section 5, "Core should expose a status signal for Core services & plugins" (from @joshdover) 6. Added the drawback that incorrect, but valid config would not block Kibana, and might only be surfaced when the associted API/UI gets used (from @azasypkin) * Formatting: number ordered list instead of letter for github rendering * Apply suggestions from code review Co-Authored-By: Josh Dover * Update rfcs/text/0007_lifecycle_unblocked.md Co-Authored-By: Josh Dover * Example of plugin exposing API dependent on internal async operation * Clarify that context providers won't block kibana, just their handlers * Update adoption strategy as per latest discussion * Fix formatting --- rfcs/text/0007_lifecycle_unblocked.md | 374 ++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 rfcs/text/0007_lifecycle_unblocked.md diff --git a/rfcs/text/0007_lifecycle_unblocked.md b/rfcs/text/0007_lifecycle_unblocked.md new file mode 100644 index 0000000000000..cb978d3dcd7ba --- /dev/null +++ b/rfcs/text/0007_lifecycle_unblocked.md @@ -0,0 +1,374 @@ +- Start Date: 2019-09-11 +- RFC PR: (leave this empty) +- Kibana Issue: (leave this empty) + +## Table of contents +- [Summary](#summary) +- [Motivation](#motivation) +- [Detailed design](#detailed-design) + - [
  1. Synchronous lifecycle methods
](#ollisynchronous-lifecycle-methodsliol) + - [
  1. Synchronous Context Provider functions
](#ol-start2lisynchronous-context-provider-functionsliol) + - [
  1. Core should not expose API's as observables
](#ol-start3licore-should-not-expose-apis-as-observablesliol) + - [
  1. Complete example code
](#ol-start4licomplete-example-codeliol) + - [
  1. Core should expose a status signal for Core services & plugins
](#ol-start5licore-should-expose-a-status-signal-for-core-services-amp-pluginsliol) +- [Drawbacks](#drawbacks) +- [Alternatives](#alternatives) + - [
  1. Introduce a lifecycle/context provider timeout
](#olliintroduce-a-lifecyclecontext-provider-timeoutliol) + - [
  1. Treat anything that blocks Kibana from starting up as a bug
](#ol-start2litreat-anything-that-blocks-kibana-from-starting-up-as-a-bugliol) +- [Adoption strategy](#adoption-strategy) +- [How we teach this](#how-we-teach-this) +- [Unresolved questions](#unresolved-questions) +- [Footnotes](#footnotes) + +# Summary + +Prevent plugin lifecycle methods from blocking Kibana startup by making the +following changes: +1. Synchronous lifecycle methods +2. Synchronous context provider functions +3. Core should not expose API's as observables + +# Motivation +Plugin lifecycle methods and context provider functions are async +(promise-returning) functions. Core runs these functions in series and waits +for each plugin's lifecycle/context provider function to resolve before +calling the next. This allows plugins to depend on the API's returned from +other plugins. + +With the current design, a single lifecycle method that blocks will block all +of Kibana from starting up. Similarly, a blocking context provider will block +all the handlers that depend on that context. Plugins (including legacy +plugins) rely heavily on this blocking behaviour to ensure that all conditions +required for their plugin's operation are met before their plugin is started +and exposes it's API's. This means a single plugin with a network error that +isn't retried or a dependency on an external host that is down, could block +all of Kibana from starting up. + +We should make it impossible for a single plugin lifecycle function to stall +all of kibana. + +# Detailed design + +### 1. Synchronous lifecycle methods +Lifecycle methods are synchronous functions, they can perform async operations +but Core doesn't wait for these to complete. This guarantees that no plugin +lifecycle function can block other plugins or core from starting up [1]. + +Core will still expose special API's that are able block the setup lifecycle +such as registering Saved Object migrations, but this will be limited to +operations where the risk of blocking all of kibana starting up is limited. + +### 2. Synchronous Context Provider functions +Making context provider functions synchronous guarantees that a context +handler will never be blocked by registered context providers. They can expose +async API's which could potentially have blocking behaviour. + +```ts +export type IContextProvider< + THandler extends HandlerFunction, + TContextName extends keyof HandlerContextType +> = ( + context: Partial>, + ...rest: HandlerParameters +) => + | HandlerContextType[TContextName]; +``` + +### 3. Core should not expose API's as observables +All Core API's should be reactive: when internal state changes, their behaviour +should change accordingly. But, exposing these internal state changes as part +of the API contract leaks internal implementation details consumers can't do +anything useful with and don't care about. + +For example: Core currently exposes `core.elasticsearch.adminClient$`, an +Observable which emits a pre-configured elasticsearch client every time there's +a configuration change. This includes changes to the logging configuration and +might in the future include updating the authentication headers sent to +elasticsearch https://github.com/elastic/kibana/issues/19829. As a plugin +author who wants to make search requests against elasticsearch I shouldn't +have to care about, react to, or keep track of, how many times the underlying +configuration has changed. I want to use the `callAsInternalUser` method and I +expect Core to use the most up to date configuration to send this request. + +> Note: It would not be desirable for Core to dynamically load all +> configuration changes. Changing the Elasticsearch `hosts` could mean Kibana +> is pointing to a completely new Elasticsearch cluster. Since this is a risky +> change to make and would likely require core and almost all plugins to +> completely re-initialize, it's safer to require a complete Kibana restart. + +This does not mean we should remove all observables from Core's API's. When an +API consumer is interested in the *state changes itself* it absolutely makes +sense to expose this as an Observable. Good examples of this is exposing +plugin config as this is state that changes over time to which a plugin should +directly react to. + +This is important in the context of synchronous lifecycle methods and context +handlers since exposing convenient API's become very ugly: + +*(3.1): exposing Observable-based API's through the route handler context:* +```ts +// Before: Using an async context provider +coreSetup.http.registerRouteHandlerContext(coreId, 'core', async (context, req) => { + const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise(); + const dataClient = await coreSetup.elasticsearch.dataClient$.pipe(take(1)).toPromise(); + return { + elasticsearch: { + adminClient: adminClient.asScoped(req), + dataClient: dataClient.asScoped(req), + }, + }; +}); + +// After: Using a synchronous context provider +coreSetup.http.registerRouteHandlerContext(coreId, 'core', async (context, req) => { + return { + elasticsearch: { + // (3.1.1) We can expose a convenient API by doing a lot of work + adminClient: () => { + callAsInternalUser: async (...args) => { + const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise(); + return adminClient.asScoped(req).callAsinternalUser(args); + }, + callAsCurrentUser: async (...args) => { + adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise(); + return adminClient.asScoped(req).callAsCurrentUser(args); + } + }, + // (3.1.2) Or a lazy approach which perpetuates the problem to consumers: + dataClient: async () => { + const dataClient = await coreSetup.elasticsearch.dataClient$.pipe(take(1)).toPromise(); + return dataClient.asScoped(req); + }, + }, + }; +}); +``` + +### 4. Complete example code +*(4.1) Doing async operations in a plugin's setup lifecycle* +```ts +export class Plugin { + public setup(core: CoreSetup) { + // Async setup is possible and any operations involving async API's + // will still block until these API's are ready, (savedObjects find only + // resolves once the elasticsearch client has established a connection to + // the cluster). The difference is that these details are now internal to + // the API. + (async () => { + const docs = await core.savedObjects.client.find({...}); + ... + await core.savedObjects.client.update(...); + })(); + } +} +``` + +*(4.2) Exposing an API from a plugin's setup lifecycle* +```ts +export class Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + private async initSavedConfig(core: CoreSetup) { + // Note: pulling a config value here means our code isn't reactive to + // changes, but this is equivalent to doing it in an async setup lifecycle. + const config = await this.initializerContext.config + .create>() + .pipe(first()) + .toPromise(); + try { + const savedConfig = await core.savedObjects.internalRepository.get({...}); + return Object.assign({}, config, savedConfig); + } catch (e) { + if (SavedObjectErrorHelpers.isNotFoundError(e)) { + return await core.savedObjects.internalRepository.create(config, {...}); + } + } + } + public setup(core: CoreSetup) { + // savedConfigPromise resolves with the same kind of "setup state" that a + // plugin would have constructed in an async setup lifecycle. + const savedConfigPromise = initSavedConfig(core); + return { + ping: async () => { + const savedConfig = await savedConfigPromise; + if (config.allowPing === false || savedConfig.allowPing === false) { + throw new Error('ping() has been disabled'); + } + // Note: the elasticsearch client no longer exposes an adminClient$ + // observable, improving the ergonomics of consuming the API. + return await core.elasticsearch.adminClient.callAsInternalUser('ping', ...); + } + }; + } +} +``` + +*(4.3) Exposing an observable free Elasticsearch API from the route context* +```ts +coreSetup.http.registerRouteHandlerContext(coreId, 'core', async (context, req) => { + return { + elasticsearch: { + adminClient: coreSetup.elasticsearch.adminClient.asScoped(req), + dataClient: coreSetup.elasticsearch.adminClient.asScoped(req), + }, + }; +}); +``` + +### 5. Core should expose a status signal for Core services & plugins +Core should expose a global mechanism for core services and plugins to signal +their status. This is equivalent to the legacy status API +`kibana.Plugin.status` which allowed plugins to set their status to e.g. 'red' +or 'green'. The exact design of this API is outside of the scope of this RFC. + +What is important, is that there is a global mechanism to signal status +changes which Core then makes visible to system administrators in the Kibana +logs and the `/status` HTTP API. Plugins should be able to inspect and +subscribe to status changes from any of their dependencies. + +This will provide an obvious mechanism for plugins to signal that the +conditions which are required for this plugin to operate are not currently +present and manual intervention might be required. Status changes can happen +in both setup and start lifecycles e.g.: + - [setup] a required remote host is down + - [start] a remote host which was up during setup, started returning + connection timeout errors. + +# Drawbacks +Not being able to block on a lifecycle method means plugins can no longer be +certain that all setup is "complete" before they expose their API's or reach +the start lifecycle. + +A plugin might want to poll an external host to ensure that the host is up in +its setup lifecycle before making network requests to this host in it's start +lifecycle. + +Even if Kibana was using a valid, but incorrect configuration for the remote +host, with synchronous lifecycles Kibana would still start up. Although the +status API and logs would indicate a problem, these might not be monitored +leading to the error only being discovered once someone tries to use it's +functionality. This is an acceptable drawback because it buys us isolation. +Some problems might go unnoticed, but no single plugin should affect the +availability of all other plugins. + +In effect, the plugin is polling the world to construct a snapshot +of state which drives future behaviour. Modeling this with lifecycle functions +is insufficient since it assumes that any state constructed in the setup +lifecycle is static and won't and can't be changed in the future. + +For example: a plugin's setup lifecycle might poll for the existence of a +custom Elasticsearch index and if it doesn't exist, create it. Should there be +an Elasticsearch restore which deletes the index, the plugin wouldn't be able +to gracefully recover by simply running it's setup lifecycle a second time. + +The once-off nature of lifecycle methods are incompatible with the real-world +dynamic conditions under which plugins run. Not being able to block a +lifecycle method is, therefore, only a drawback when plugins are authored under +the false illusion of stability. + +# Alternatives +## 1. Introduce a lifecycle/context provider timeout +Lifecycle methods and context providers would timeout after X seconds and any +API's they expose would not be available if the timeout had been reached. + +Drawbacks: +1. A blocking setup lifecycle makes it easy for plugin authors to fall into + the trap of assuming that their plugin's behaviour can continue to operate + based on the snapshot of conditions present during setup. + +2. For lifecycle methods: there would be no way to recover from a timeout, + once a timeout had been reached the API will remain unavailable. + + Context providers have the benefit of being re-created for each handler + call, so a single timeout would not permanently disable the API. + +3. Plugins have less control over their behaviour. When an upstream server + becomes unavailable, a plugin might prefer to keep retrying the request + indefinitely or only timeout after more than X seconds. It also isn't able + to expose detailed error information to downstream consumers such as + specifying which host or service is unavailable. + +4. (minor) Introduces an additional failure condition that needs to be handled. + Consumers should handle the API not being available in setup, as well as, + error responses from the API itself. Since remote hosts like Elasticsearch + could go down even after a successful setup, this effectively means API + consumers have to handle the same error condition in two places. + +## 2. Treat anything that blocks Kibana from starting up as a bug +Keep the existing New Platform blocking behaviour, but through strong +conventions and developer awareness minimize the risk of plugins blocking +Kibana's startup indefinetely. By logging detailed diagnostic info on any +plugins that appear to be blocking startup, we can aid system administrators +to recover a blocked Kibana. + +A parallel can be drawn between Kibana's async plugin initialization and the TC39 +proposal for [top-level await](https://github.com/tc39/proposal-top-level-await). +> enables modules to act as big async functions: With top-level await, +> ECMAScript Modules (ESM) can await resources, causing other modules who +> import them to wait before they start evaluating their body + +They believe the benefits outweigh the risk of modules blocking loading since: + - [developer education should result in correct usage](https://github.com/tc39/proposal-top-level-await#will-top-level-await-cause-developers-to-make-their-code-block-longer-than-it-should) + - [there are existing unavoidable ways in which modules could block loading such as infinite loops or recursion](https://github.com/tc39/proposal-top-level-await#does-top-level-await-increase-the-risk-of-deadlocks) + + +Drawbacks: +1. A blocking setup lifecycle makes it easy for plugin authors to fall into + the trap of assuming that their plugin's behaviour can continue to operate + based on the snapshot of conditions present during setup. +2. This opens up the potential for a bug in Elastic or third-party plugins to + effectively "break" kibana. Instead of a single plugin being disabled all + of kibana would be down requiring manual intervention by a system + administrator. + +# Adoption strategy +Although the eventual goal is to have sync-only lifecycles / providers, we +will start by deprecating async behaviour and implementing a 30s timeout as +per alternative (1). This will immediately lower the impact of plugin bugs +while at the same time enabling a more incremental rollout and the flexibility +to discover use cases that would require adopting Core API's to support sync +lifecycles / providers. + +Adoption and implementation should be handled as follows: + - Adopt Core API’s to make sync lifecycles easier (3) + - Update migration guide and other documentation examples. + - Deprecate async lifecycles / context providers with a warning. Add a + timeout of 30s after which a plugin and it's dependencies will be disabled. + - Refactor existing plugin lifecycles which are easily converted to sync + - Future: remove async timeout lifecycles / context providers + +The following New Platform plugins or shims currently rely on async lifecycle +functions and will be impacted: +1. [region_map](https://github.com/elastic/kibana/blob/6039709929caf0090a4130b8235f3a53bd04ed84/src/legacy/core_plugins/region_map/public/plugin.ts#L68) +2. [tile_map](https://github.com/elastic/kibana/blob/6039709929caf0090a4130b8235f3a53bd04ed84/src/legacy/core_plugins/tile_map/public/plugin.ts#L62) +3. [vis_type_table](https://github.com/elastic/kibana/blob/6039709929caf0090a4130b8235f3a53bd04ed84/src/legacy/core_plugins/vis_type_table/public/plugin.ts#L61) +4. [vis_type_vega](https://github.com/elastic/kibana/blob/6039709929caf0090a4130b8235f3a53bd04ed84/src/legacy/core_plugins/vis_type_vega/public/plugin.ts#L59) +5. [timelion](https://github.com/elastic/kibana/blob/9d69b72a5f200e58220231035b19da852fc6b0a5/src/plugins/timelion/server/plugin.ts#L40) +6. [code](https://github.com/elastic/kibana/blob/5049b460b47d4ae3432e1d9219263bb4be441392/x-pack/legacy/plugins/code/server/plugin.ts#L129-L149) +7. [spaces](https://github.com/elastic/kibana/blob/096c7ee51136327f778845c636d7c4f1188e5db2/x-pack/legacy/plugins/spaces/server/new_platform/plugin.ts#L95) +8. [licensing](https://github.com/elastic/kibana/blob/4667c46caef26f8f47714504879197708debae32/x-pack/plugins/licensing/server/plugin.ts) +9. [security](https://github.com/elastic/kibana/blob/0f2324e44566ce2cf083d89082841e57d2db6ef6/x-pack/plugins/security/server/plugin.ts#L96) + +# How we teach this + +Async Plugin lifecycle methods and async context provider functions have been +deprecated. In the future all lifecycle methods will by sync only. Plugins +should treat the setup lifecycle as a place in time to register functionality +with core or other plugins' API's and not as a mechanism to kick off and wait +for any initialization that's required for the plugin to be able to run. + +# Unresolved questions +1. ~~Are the drawbacks worth the benefits or can we live with Kibana potentially +being blocked for the sake of convenient async lifecycle stages?~~ + +2. Should core provide conventions or patterns for plugins to construct a + snapshot of state and reactively updating this state and the behaviour it + drives as the state of the world changes? + +3. Do plugins ever need to read config values and pass these as parameters to + Core API’s? If so we would have to expose synchronous config values to + support sync lifecycles. + +# Footnotes +[1] Synchronous lifecycles can still be blocked by e.g. an infine for loop, +but this would always be unintentional behaviour in contrast to intentional +async behaviour like blocking until an external service becomes available. From 815f72155555c66d9d86e8446f705827ca791e58 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 18 Dec 2019 15:12:02 +0100 Subject: [PATCH 03/61] [APM] Add version annotations to timeseries charts (#52640) * [APM] Add version annotations to timeseries charts Closes #51426. * Don't subdue 'Version' text in tooltip * Optimize version queries * Don't pass radius/color to indicator --- .../legacy/plugins/apm/common/annotations.ts | 16 +++ .../charts/CustomPlot/AnnotationsPlot.tsx | 74 ++++++++++++ .../shared/charts/CustomPlot/Legends.js | 38 +++++- .../shared/charts/CustomPlot/index.js | 30 ++++- .../charts/CustomPlot/plotUtils.test.ts | 6 +- .../{plotUtils.js => plotUtils.tsx} | 46 +++++-- .../__snapshots__/CustomPlot.test.js.snap | 11 ++ .../components/shared/charts/Legend/index.js | 3 +- .../apm/public/context/ChartsSyncContext.tsx | 32 ++++- .../plugins/apm/public/utils/testHelpers.tsx | 61 ++++++---- .../__fixtures__/multiple-versions.json | 34 ++++++ .../annotations/__fixtures__/no-versions.json | 25 ++++ .../annotations/__fixtures__/one-version.json | 30 +++++ .../__fixtures__/versions-first-seen.json | 24 ++++ .../lib/services/annotations/index.test.ts | 98 +++++++++++++++ .../server/lib/services/annotations/index.ts | 114 ++++++++++++++++++ .../apm/server/routes/create_apm_api.ts | 4 +- .../plugins/apm/server/routes/services.ts | 27 +++++ .../legacy/plugins/apm/typings/react-vis.d.ts | 7 ++ .../legacy/plugins/apm/typings/timeseries.ts | 6 +- 20 files changed, 639 insertions(+), 47 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/common/annotations.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx rename x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/{plotUtils.js => plotUtils.tsx} (74%) create mode 100644 x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/multiple-versions.json create mode 100644 x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/no-versions.json create mode 100644 x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/one-version.json create mode 100644 x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/versions-first-seen.json create mode 100644 x-pack/legacy/plugins/apm/server/lib/services/annotations/index.test.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/services/annotations/index.ts create mode 100644 x-pack/legacy/plugins/apm/typings/react-vis.d.ts diff --git a/x-pack/legacy/plugins/apm/common/annotations.ts b/x-pack/legacy/plugins/apm/common/annotations.ts new file mode 100644 index 0000000000000..33122f55d8800 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/annotations.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. + */ + +export enum AnnotationType { + VERSION = 'version' +} + +export interface Annotation { + type: AnnotationType; + id: string; + time: number; + text: string; +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx new file mode 100644 index 0000000000000..fb087612f8e3d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.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 { VerticalGridLines } from 'react-vis'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { + EuiIcon, + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + EuiText +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Maybe } from '../../../../../typings/common'; +import { Annotation } from '../../../../../common/annotations'; +import { PlotValues, SharedPlot } from './plotUtils'; +import { asAbsoluteDateTime } from '../../../../utils/formatters'; + +interface Props { + annotations: Annotation[]; + plotValues: PlotValues; + width: number; + overlay: Maybe; +} + +const style = { + stroke: theme.euiColorSecondary, + strokeDasharray: 'none' +}; + +export function AnnotationsPlot(props: Props) { + const { plotValues, annotations } = props; + + const tickValues = annotations.map(annotation => annotation.time); + + return ( + <> + + + + {annotations.map(annotation => ( +
+ + + + {i18n.translate('xpack.apm.version', { + defaultMessage: 'Version' + })} + + + {annotation.text} + + } + > + + +
+ ))} + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js index 11755e13bfdd6..848c975942ff6 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js @@ -16,6 +16,8 @@ import { truncate } from '../../../../style/variables'; import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon } from '@elastic/eui'; const Container = styled.div` display: flex; @@ -73,9 +75,12 @@ export default function Legends({ noHits, series, seriesEnabledState, - truncateLegends + truncateLegends, + hasAnnotations, + showAnnotations, + onAnnotationsToggle }) { - if (noHits) { + if (noHits && !hasAnnotations) { return null; } @@ -107,6 +112,30 @@ export default function Legends({ /> ); })} + {hasAnnotations && ( + { + if (onAnnotationsToggle) { + onAnnotationsToggle(); + } + }} + text={ + + {i18n.translate('xpack.apm.serviceVersion', { + defaultMessage: 'Service version' + })} + + } + indicator={() => ( +
+ +
+ )} + disabled={!showAnnotations} + color={theme.euiColorSecondary} + /> + )} ); @@ -118,5 +147,8 @@ Legends.propTypes = { noHits: PropTypes.bool.isRequired, series: PropTypes.array.isRequired, seriesEnabledState: PropTypes.array.isRequired, - truncateLegends: PropTypes.bool.isRequired + truncateLegends: PropTypes.bool.isRequired, + hasAnnotations: PropTypes.bool, + showAnnotations: PropTypes.bool, + onAnnotationsToggle: PropTypes.func }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/index.js index e87d60c9b3fe8..f59c30d2f4d2f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/index.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/index.js @@ -13,6 +13,7 @@ import Legends from './Legends'; import StaticPlot from './StaticPlot'; import InteractivePlot from './InteractivePlot'; import VoronoiPlot from './VoronoiPlot'; +import { AnnotationsPlot } from './AnnotationsPlot'; import { createSelector } from 'reselect'; import { getPlotValues } from './plotUtils'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; @@ -28,7 +29,8 @@ export class InnerCustomPlot extends PureComponent { seriesEnabledState: [], isDrawing: false, selectionStart: null, - selectionEnd: null + selectionEnd: null, + showAnnotations: true }; getEnabledSeries = createSelector( @@ -122,7 +124,7 @@ export class InnerCustomPlot extends PureComponent { } render() { - const { series, truncateLegends, width } = this.props; + const { series, truncateLegends, width, annotations } = this.props; if (!width) { return null; @@ -166,6 +168,14 @@ export class InnerCustomPlot extends PureComponent { tickFormatX={this.props.tickFormatX} /> + {this.state.showAnnotations && !isEmpty(annotations) && ( + + )} + { + this.setState(({ showAnnotations }) => ({ + showAnnotations: !showAnnotations + })); + }} /> ); @@ -209,7 +226,14 @@ InnerCustomPlot.propTypes = { truncateLegends: PropTypes.bool, width: PropTypes.number.isRequired, height: PropTypes.number, - stackBy: PropTypes.string + stackBy: PropTypes.string, + annotations: PropTypes.arrayOf( + PropTypes.shape({ + type: PropTypes.string, + id: PropTypes.string, + firstSeen: PropTypes.number + }) + ) }; InnerCustomPlot.defaultProps = { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts index 55bfb490e8588..b130deed7f098 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts @@ -6,6 +6,7 @@ // @ts-ignore import * as plotUtils from './plotUtils'; +import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; describe('plotUtils', () => { describe('getPlotValues', () => { @@ -34,7 +35,10 @@ describe('plotUtils', () => { expect( plotUtils .getPlotValues( - [{ data: { x: 0, y: 200 } }, { data: { x: 0, y: 300 } }], + [ + { data: [{ x: 0, y: 200 }] }, + { data: [{ x: 0, y: 300 }] } + ] as Array>, [], { height: 1, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx similarity index 74% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.js rename to x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx index 4186f6c899750..10eb4659ea695 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx @@ -11,6 +11,7 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import React from 'react'; +import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; import { unit } from '../../../../style/variables'; import { getTimezoneOffsetInMs } from './getTimezoneOffsetInMs'; @@ -22,20 +23,23 @@ const XY_MARGIN = { bottom: unit * 2 }; -const getXScale = (xMin, xMax, width) => { +const getXScale = (xMin: number, xMax: number, width: number) => { return scaleLinear() .domain([xMin, xMax]) .range([XY_MARGIN.left, width - XY_MARGIN.right]); }; -const getYScale = (yMin, yMax) => { +const getYScale = (yMin: number, yMax: number) => { return scaleLinear() .domain([yMin, yMax]) .range([XY_HEIGHT, 0]) .nice(); }; -function getFlattenedCoordinates(visibleSeries, enabledSeries) { +function getFlattenedCoordinates( + visibleSeries: Array>, + enabledSeries: Array> +) { const enabledCoordinates = flatten(enabledSeries.map(serie => serie.data)); if (!isEmpty(enabledCoordinates)) { return enabledCoordinates; @@ -44,10 +48,24 @@ function getFlattenedCoordinates(visibleSeries, enabledSeries) { return flatten(visibleSeries.map(serie => serie.data)); } +export type PlotValues = ReturnType; + export function getPlotValues( - visibleSeries, - enabledSeries, - { width, yMin = 0, yMax = 'max', height, stackBy } + visibleSeries: Array>, + enabledSeries: Array>, + { + width, + yMin = 0, + yMax = 'max', + height, + stackBy + }: { + width: number; + yMin?: number | 'min'; + yMax?: number | 'max'; + height: number; + stackBy?: 'x' | 'y'; + } ) { const flattenedCoordinates = getFlattenedCoordinates( visibleSeries, @@ -59,10 +77,10 @@ export function getPlotValues( const xMax = d3.max(flattenedCoordinates, d => d.x); if (yMax === 'max') { - yMax = d3.max(flattenedCoordinates, d => d.y); + yMax = d3.max(flattenedCoordinates, d => d.y ?? 0); } if (yMin === 'min') { - yMin = d3.min(flattenedCoordinates, d => d.y); + yMin = d3.min(flattenedCoordinates, d => d.y ?? 0); } const [xMinZone, xMaxZone] = [xMin, xMax].map(x => { @@ -101,11 +119,19 @@ export function getPlotValues( }; } -export function SharedPlot({ plotValues, ...props }) { +export function SharedPlot({ + plotValues, + ...props +}: { + plotValues: PlotValues; + children: React.ReactNode; +}) { const { XY_HEIGHT: height, XY_MARGIN: margin, XY_WIDTH: width } = plotValues; return ( -
+
- + {indicator ? indicator() : } {text} ); diff --git a/x-pack/legacy/plugins/apm/public/context/ChartsSyncContext.tsx b/x-pack/legacy/plugins/apm/public/context/ChartsSyncContext.tsx index c2676a35d8e78..afce0811b48f6 100644 --- a/x-pack/legacy/plugins/apm/public/context/ChartsSyncContext.tsx +++ b/x-pack/legacy/plugins/apm/public/context/ChartsSyncContext.tsx @@ -7,6 +7,8 @@ import React, { useMemo, useState } from 'react'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { history } from '../utils/history'; +import { useUrlParams } from '../hooks/useUrlParams'; +import { useFetcher } from '../hooks/useFetcher'; const ChartsSyncContext = React.createContext<{ hoverX: number | null; @@ -17,6 +19,31 @@ const ChartsSyncContext = React.createContext<{ const ChartsSyncContextProvider: React.FC = ({ children }) => { const [time, setTime] = useState(null); + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end, serviceName } = urlParams; + const { environment } = uiFilters; + + const { data = { annotations: [] } } = useFetcher( + callApmApi => { + if (start && end && serviceName) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/annotations', + params: { + path: { + serviceName + }, + query: { + start, + end, + environment + } + } + }); + } + }, + [start, end, environment, serviceName] + ); const value = useMemo(() => { const hoverXHandlers = { @@ -43,11 +70,12 @@ const ChartsSyncContextProvider: React.FC = ({ children }) => { }) }); }, - hoverX: time + hoverX: time, + annotations: data.annotations }; return { ...hoverXHandlers }; - }, [time, setTime]); + }, [time, data.annotations]); return ; }; diff --git a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx index 0c8a7cbc17884..862c982d6b5ac 100644 --- a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx @@ -19,7 +19,11 @@ import { MemoryRouter } from 'react-router-dom'; import { APMConfig } from '../../../../../plugins/apm/server'; import { LocationProvider } from '../context/LocationContext'; import { PromiseReturnType } from '../../typings/common'; -import { ESFilter } from '../../typings/elasticsearch'; +import { + ESFilter, + ESSearchResponse, + ESSearchRequest +} from '../../typings/elasticsearch'; import { ApmPluginContext, ApmPluginContextValue @@ -117,29 +121,41 @@ interface MockSetup { }; } +interface Options { + mockResponse?: ( + request: ESSearchRequest + ) => ESSearchResponse; +} + export async function inspectSearchParams( - fn: (mockSetup: MockSetup) => Promise + fn: (mockSetup: MockSetup) => Promise, + options: Options = {} ) { - const clientSpy = jest.fn().mockReturnValueOnce({ - hits: { - total: 0 - } + const spy = jest.fn().mockImplementation(async request => { + return options.mockResponse + ? options.mockResponse(request) + : { + hits: { + hits: { + total: { + value: 0 + } + } + } + }; }); - const internalClientSpy = jest.fn().mockReturnValueOnce({ - hits: { - total: 0 - } - }); + let response; + let error; const mockSetup = { start: 1528113600000, end: 1528977600000, client: { - search: clientSpy + search: spy } as any, internalClient: { - search: internalClientSpy + search: spy } as any, config: new Proxy( {}, @@ -164,21 +180,18 @@ export async function inspectSearchParams( dynamicIndexPattern: null as any }; try { - await fn(mockSetup); - } catch { + response = await fn(mockSetup); + } catch (err) { + error = err; // we're only extracting the search params } - let params; - if (clientSpy.mock.calls.length) { - params = clientSpy.mock.calls[0][0]; - } else { - params = internalClientSpy.mock.calls[0][0]; - } - return { - params, - teardown: () => clientSpy.mockClear() + params: spy.mock.calls[0][0], + response, + error, + spy, + teardown: () => spy.mockClear() }; } diff --git a/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/multiple-versions.json b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/multiple-versions.json new file mode 100644 index 0000000000000..7e2d2405d681c --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/multiple-versions.json @@ -0,0 +1,34 @@ +{ + "took": 444, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 10000, + "relation": "gte" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "versions": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "8.0.0", + "doc_count": 615285 + }, + { + "key": "7.5.0", + "doc_count": 615285 + } + ] + } + } +} diff --git a/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/no-versions.json b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/no-versions.json new file mode 100644 index 0000000000000..fa5c63f1b9a54 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/no-versions.json @@ -0,0 +1,25 @@ +{ + "took": 398, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 10000, + "relation": "gte" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "versions": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [] + } + } +} diff --git a/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/one-version.json b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/one-version.json new file mode 100644 index 0000000000000..56303909bcd6f --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/one-version.json @@ -0,0 +1,30 @@ +{ + "took": 444, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 10000, + "relation": "gte" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "versions": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "8.0.0", + "doc_count": 615285 + } + ] + } + } +} diff --git a/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/versions-first-seen.json b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/versions-first-seen.json new file mode 100644 index 0000000000000..c53b28c8bf594 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/annotations/__fixtures__/versions-first-seen.json @@ -0,0 +1,24 @@ +{ + "took": 4750, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 10000, + "relation": "gte" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "first_seen": { + "value": 1.5281138E12, + "value_as_string": "2018-06-04T12:00:00.000Z" + } + } +} diff --git a/x-pack/legacy/plugins/apm/server/lib/services/annotations/index.test.ts b/x-pack/legacy/plugins/apm/server/lib/services/annotations/index.test.ts new file mode 100644 index 0000000000000..75ac0642a1b8c --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/annotations/index.test.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 { getServiceAnnotations } from '.'; +import { + SearchParamsMock, + inspectSearchParams +} 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'; +import versionsFirstSeen from './__fixtures__/versions-first-seen.json'; + +describe('getServiceAnnotations', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + describe('with 0 versions', () => { + it('returns no annotations', async () => { + mock = await inspectSearchParams( + setup => + getServiceAnnotations({ + setup, + serviceName: 'foo', + environment: 'bar' + }), + { + mockResponse: () => noVersions + } + ); + + expect(mock.response).toEqual({ annotations: [] }); + }); + }); + + describe('with 1 version', () => { + it('returns no annotations', async () => { + mock = await inspectSearchParams( + setup => + getServiceAnnotations({ + setup, + serviceName: 'foo', + environment: 'bar' + }), + { + mockResponse: () => oneVersion + } + ); + + expect(mock.response).toEqual({ annotations: [] }); + }); + }); + + describe('with more than 1 version', () => { + it('returns two annotations', async () => { + const responses = [ + multipleVersions, + versionsFirstSeen, + versionsFirstSeen + ]; + mock = await inspectSearchParams( + setup => + getServiceAnnotations({ + setup, + serviceName: 'foo', + environment: 'bar' + }), + { + mockResponse: () => responses.shift() + } + ); + + expect(mock.spy.mock.calls.length).toBe(3); + + expect(mock.response).toEqual({ + annotations: [ + { + id: '8.0.0', + text: '8.0.0', + time: 1.5281138e12, + type: 'version' + }, + { + id: '7.5.0', + text: '7.5.0', + time: 1.5281138e12, + type: 'version' + } + ] + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/services/annotations/index.ts b/x-pack/legacy/plugins/apm/server/lib/services/annotations/index.ts new file mode 100644 index 0000000000000..c03746ca220ee --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/services/annotations/index.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 { isNumber } from 'lodash'; +import { Annotation, AnnotationType } from '../../../../common/annotations'; +import { ESFilter } from '../../../../typings/elasticsearch'; +import { + SERVICE_NAME, + SERVICE_ENVIRONMENT, + PROCESSOR_EVENT +} from '../../../../common/elasticsearch_fieldnames'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { rangeFilter } from '../../helpers/range_filter'; +import { SERVICE_VERSION } from '../../../../common/elasticsearch_fieldnames'; + +export async function getServiceAnnotations({ + setup, + serviceName, + environment +}: { + serviceName: string; + environment?: string; + setup: Setup & SetupTimeRange; +}) { + const { start, end, client, indices } = setup; + + const filter: ESFilter[] = [ + { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { range: rangeFilter(start, end) }, + { term: { [SERVICE_NAME]: serviceName } } + ]; + + if (environment) { + filter.push({ term: { [SERVICE_ENVIRONMENT]: environment } }); + } + + const versions = + ( + await client.search({ + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + track_total_hits: false, + query: { + bool: { + filter + } + }, + aggs: { + versions: { + terms: { + field: SERVICE_VERSION + } + } + } + } + }) + ).aggregations?.versions.buckets.map(bucket => bucket.key) ?? []; + + if (versions.length > 1) { + const annotations = await Promise.all( + versions.map(async version => { + const response = await client.search({ + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + query: { + bool: { + filter: filter + .filter(esFilter => !Object.keys(esFilter).includes('range')) + .concat({ + term: { + [SERVICE_VERSION]: version + } + }) + } + }, + aggs: { + first_seen: { + min: { + field: '@timestamp' + } + } + }, + track_total_hits: false + } + }); + + const firstSeen = response.aggregations?.first_seen.value; + + if (!isNumber(firstSeen)) { + throw new Error( + 'First seen for version was unexpectedly undefined or null.' + ); + } + + if (firstSeen < start || firstSeen > end) { + return null; + } + + return { + type: AnnotationType.VERSION, + id: version, + time: firstSeen, + text: version + }; + }) + ); + return { annotations: annotations.filter(Boolean) as Annotation[] }; + } + return { annotations: [] }; +} diff --git a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts index 95488591d4b89..e98842151da84 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts @@ -17,7 +17,8 @@ import { serviceAgentNameRoute, serviceTransactionTypesRoute, servicesRoute, - serviceNodeMetadataRoute + serviceNodeMetadataRoute, + serviceAnnotationsRoute } from './services'; import { agentConfigurationRoute, @@ -75,6 +76,7 @@ const createApmApi = () => { .add(serviceTransactionTypesRoute) .add(servicesRoute) .add(serviceNodeMetadataRoute) + .add(serviceAnnotationsRoute) // Agent configuration .add(agentConfigurationAgentNameRoute) diff --git a/x-pack/legacy/plugins/apm/server/routes/services.ts b/x-pack/legacy/plugins/apm/server/routes/services.ts index 91495bb96b032..78cb092b85db6 100644 --- a/x-pack/legacy/plugins/apm/server/routes/services.ts +++ b/x-pack/legacy/plugins/apm/server/routes/services.ts @@ -19,6 +19,7 @@ import { getServiceNodeMetadata } from '../lib/services/get_service_node_metadat import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceMap } from '../lib/services/map'; +import { getServiceAnnotations } from '../lib/services/annotations'; export const servicesRoute = createRoute(() => ({ path: '/api/apm/services', @@ -98,3 +99,29 @@ export const serviceMapRoute = createRoute(() => ({ return new Boom('Not found', { statusCode: 404 }); } })); + +export const serviceAnnotationsRoute = createRoute(() => ({ + path: '/api/apm/services/{serviceName}/annotations', + params: { + path: t.type({ + serviceName: t.string + }), + query: t.intersection([ + rangeRt, + t.partial({ + environment: t.string + }) + ]) + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.path; + const { environment } = context.params.query; + + return getServiceAnnotations({ + setup, + serviceName, + environment + }); + } +})); diff --git a/x-pack/legacy/plugins/apm/typings/react-vis.d.ts b/x-pack/legacy/plugins/apm/typings/react-vis.d.ts new file mode 100644 index 0000000000000..aef8efc30d555 --- /dev/null +++ b/x-pack/legacy/plugins/apm/typings/react-vis.d.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. + */ + +declare module 'react-vis'; diff --git a/x-pack/legacy/plugins/apm/typings/timeseries.ts b/x-pack/legacy/plugins/apm/typings/timeseries.ts index d64486d8e71e9..600be15ea229f 100644 --- a/x-pack/legacy/plugins/apm/typings/timeseries.ts +++ b/x-pack/legacy/plugins/apm/typings/timeseries.ts @@ -15,12 +15,14 @@ export interface RectCoordinate { x0: number; } -export interface TimeSeries { +export interface TimeSeries< + TCoordinate extends { x: number } = Coordinate | RectCoordinate +> { title: string; titleShort?: string; hideLegend?: boolean; hideTooltipValue?: boolean; - data: Array; + data: TCoordinate[]; legendValue?: string; type: string; color: string; From 966dd82b6474566d647b81a504eda502f60661f1 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 18 Dec 2019 07:48:56 -0700 Subject: [PATCH 04/61] [Maps] gather field formatters in data request (#53311) * [Maps] gather field formatters in data request so they can be used sync in vector_style * review feedback * hasMatchingMetricField * review feedback * fix typo in function name --- .../legacy/plugins/maps/common/constants.js | 2 + .../maps/public/layers/joins/inner_join.js | 6 +- .../public/layers/sources/es_agg_source.js | 5 + .../layers/styles/vector/vector_style.js | 60 +++++++++--- .../maps/public/layers/util/can_skip_fetch.js | 12 +++ .../maps/public/layers/vector_layer.js | 93 +++++++++++++++++-- 6 files changed, 156 insertions(+), 22 deletions(-) diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index afe30f3492147..a6bdee3d2ed12 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -59,6 +59,8 @@ export const FIELD_ORIGIN = { export const SOURCE_DATA_ID_ORIGIN = 'source'; export const META_ID_ORIGIN_SUFFIX = 'meta'; export const SOURCE_META_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${META_ID_ORIGIN_SUFFIX}`; +export const FORMATTERS_ID_ORIGIN_SUFFIX = 'formatters'; +export const SOURCE_FORMATTERS_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${FORMATTERS_ID_ORIGIN_SUFFIX}`; export const GEOJSON_FILE = 'GEOJSON_FILE'; diff --git a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js index 6671914bce74f..13a2e05ab8eeb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js +++ b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js @@ -6,7 +6,7 @@ import { ESTermSource } from '../sources/es_term_source'; import { getComputedFieldNamePrefix } from '../styles/vector/style_util'; -import { META_ID_ORIGIN_SUFFIX } from '../../../common/constants'; +import { META_ID_ORIGIN_SUFFIX, FORMATTERS_ID_ORIGIN_SUFFIX } from '../../../common/constants'; export class InnerJoin { constructor(joinDescriptor, leftSource) { @@ -45,6 +45,10 @@ export class InnerJoin { return `${this.getSourceDataRequestId()}_${META_ID_ORIGIN_SUFFIX}`; } + getSourceFormattersDataRequestId() { + return `${this.getSourceDataRequestId()}_${FORMATTERS_ID_ORIGIN_SUFFIX}`; + } + getLeftField() { return this._leftField; } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js index c07b51c20ab85..fd3ae8f0ab7e3 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_agg_source.js @@ -82,6 +82,11 @@ export class AbstractESAggSource extends AbstractESSource { }); } + hasMatchingMetricField(fieldName) { + const matchingField = this.getMetricFieldForName(fieldName); + return !!matchingField; + } + getMetricFieldForName(fieldName) { return this.getMetricFields().find(metricField => { return metricField.getName() === fieldName; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index d386c0ad4a5e0..161c0ea69e86c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -19,6 +19,7 @@ import { FIELD_ORIGIN, STYLE_TYPE, SOURCE_META_ID_ORIGIN, + SOURCE_FORMATTERS_ID_ORIGIN, LAYER_STYLE_TYPE, } from '../../../../common/constants'; import { VectorIcon } from './components/legend/vector_icon'; @@ -294,14 +295,17 @@ export class VectorStyle extends AbstractStyle { return this._isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POLYGON); }; - _getFieldMeta = fieldName => { - const fieldMetaFromLocalFeatures = _.get(this._descriptor, ['__styleMeta', fieldName]); - + _getDynamicPropertyByFieldName(fieldName) { const dynamicProps = this.getDynamicPropertiesArray(); - const dynamicProp = dynamicProps.find(dynamicProp => { + return dynamicProps.find(dynamicProp => { return fieldName === dynamicProp.getField().getName(); }); + } + _getFieldMeta = fieldName => { + const fieldMetaFromLocalFeatures = _.get(this._descriptor, ['__styleMeta', fieldName]); + + const dynamicProp = this._getDynamicPropertyByFieldName(fieldName); if (!dynamicProp || !dynamicProp.isFieldMetaEnabled()) { return fieldMetaFromLocalFeatures; } @@ -311,8 +315,7 @@ export class VectorStyle extends AbstractStyle { dataRequestId = SOURCE_META_ID_ORIGIN; } else { const join = this._layer.getValidJoins().find(join => { - const matchingField = join.getRightJoinSource().getMetricFieldForName(fieldName); - return !!matchingField; + return join.getRightJoinSource().hasMatchingMetricField(fieldName); }); if (join) { dataRequestId = join.getSourceMetaDataRequestId(); @@ -323,7 +326,7 @@ export class VectorStyle extends AbstractStyle { return fieldMetaFromLocalFeatures; } - const styleMetaDataRequest = this._layer._findDataRequestForSource(dataRequestId); + const styleMetaDataRequest = this._layer._findDataRequestById(dataRequestId); if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { return fieldMetaFromLocalFeatures; } @@ -334,6 +337,37 @@ export class VectorStyle extends AbstractStyle { return fieldMeta ? fieldMeta : fieldMetaFromLocalFeatures; }; + _getFieldFormatter(fieldName) { + const dynamicProp = this._getDynamicPropertyByFieldName(fieldName); + if (!dynamicProp) { + return null; + } + + let dataRequestId; + if (dynamicProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { + dataRequestId = SOURCE_FORMATTERS_ID_ORIGIN; + } else { + const join = this._layer.getValidJoins().find(join => { + return join.getRightJoinSource().hasMatchingMetricField(fieldName); + }); + if (join) { + dataRequestId = join.getSourceFormattersDataRequestId(); + } + } + + if (!dataRequestId) { + return null; + } + + const formattersDataRequest = this._layer._findDataRequestById(dataRequestId); + if (!formattersDataRequest || !formattersDataRequest.hasData()) { + return null; + } + + const formatters = formattersDataRequest.getData(); + return formatters[fieldName]; + } + _getStyleMeta = () => { return _.get(this._descriptor, '__styleMeta', {}); }; @@ -382,7 +416,7 @@ export class VectorStyle extends AbstractStyle { const promises = styles.map(async style => { return { label: await style.getField().getLabel(), - fieldFormatter: await this._source.getFieldFormatter(style.getField().getName()), + fieldFormatter: this._getFieldFormatter(style.getField().getName()), meta: this._getFieldMeta(style.getField().getName()), style, }; @@ -539,14 +573,10 @@ export class VectorStyle extends AbstractStyle { fieldName: fieldDescriptor.name, }); } else if (fieldDescriptor.origin === FIELD_ORIGIN.JOIN) { - let matchingField = null; - const joins = this._layer.getValidJoins(); - joins.find(join => { - const aggSource = join.getRightJoinSource(); - matchingField = aggSource.getMetricFieldForName(fieldDescriptor.name); - return !!matchingField; + const join = this._layer.getValidJoins().find(join => { + return join.getRightJoinSource().hasMatchingMetricField(fieldDescriptor.name); }); - return matchingField; + return join ? join.getRightJoinSource().getMetricFieldForName(fieldDescriptor.name) : null; } else { throw new Error(`Unknown origin-type ${fieldDescriptor.origin}`); } diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js index 41f6de65e4032..d9182be56a75f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js @@ -158,3 +158,15 @@ export function canSkipStyleMetaUpdate({ prevDataRequest, nextMeta }) { !updateDueToFields && !updateDueToSourceQuery && !updateDueToIsTimeAware && !updateDueToTime ); } + +export function canSkipFormattersUpdate({ prevDataRequest, nextMeta }) { + if (!prevDataRequest) { + return false; + } + const prevMeta = prevDataRequest.getMeta(); + if (!prevMeta) { + return false; + } + + return !_.isEqual(prevMeta.fieldNames, nextMeta.fieldNames); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 16129087de1f8..30c47658bb327 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -13,6 +13,7 @@ import { FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN, SOURCE_META_ID_ORIGIN, + SOURCE_FORMATTERS_ID_ORIGIN, FEATURE_VISIBLE_PROPERTY_NAME, EMPTY_FEATURE_COLLECTION, LAYER_TYPE, @@ -24,7 +25,11 @@ import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; -import { canSkipSourceUpdate, canSkipStyleMetaUpdate } from './util/can_skip_fetch'; +import { + canSkipSourceUpdate, + canSkipStyleMetaUpdate, + canSkipFormattersUpdate, +} from './util/can_skip_fetch'; import { assignFeatureIds } from './util/assign_feature_ids'; import { getFillFilterExpression, @@ -220,7 +225,7 @@ export class VectorLayer extends AbstractLayer { return indexPatternIds; } - _findDataRequestForSource(sourceDataId) { + _findDataRequestById(sourceDataId) { return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId); } @@ -241,7 +246,7 @@ export class VectorLayer extends AbstractLayer { sourceQuery: joinSource.getWhereQuery(), applyGlobalQuery: joinSource.getApplyGlobalQuery(), }; - const prevDataRequest = this._findDataRequestForSource(sourceDataId); + const prevDataRequest = this._findDataRequestById(sourceDataId); const canSkipFetch = await canSkipSourceUpdate({ source: joinSource, @@ -286,6 +291,7 @@ export class VectorLayer extends AbstractLayer { async _syncJoins(syncContext) { const joinSyncs = this.getValidJoins().map(async join => { await this._syncJoinStyleMeta(syncContext, join); + await this._syncJoinFormatters(syncContext, join); return this._syncJoin({ join, ...syncContext }); }); @@ -355,7 +361,7 @@ export class VectorLayer extends AbstractLayer { registerCancelCallback, dataFilters, }) { - const requestToken = Symbol(`layer-source-data:${this.getId()}`); + const requestToken = Symbol(`layer-${this.getId()}-${SOURCE_DATA_ID_ORIGIN}`); const searchFilters = this._getSearchFilters(dataFilters); const prevDataRequest = this.getSourceDataRequest(); @@ -459,13 +465,13 @@ export class VectorLayer extends AbstractLayer { isTimeAware: this._style.isTimeAware() && (await source.isTimeAware()), timeFilters: dataFilters.timeFilters, }; - const prevDataRequest = this._findDataRequestForSource(dataRequestId); + const prevDataRequest = this._findDataRequestById(dataRequestId); const canSkipFetch = canSkipStyleMetaUpdate({ prevDataRequest, nextMeta }); if (canSkipFetch) { return; } - const requestToken = Symbol(`layer-${this.getId()}-style-meta`); + const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); try { startLoading(dataRequestId, requestToken, nextMeta); const layerName = await this.getDisplayName(); @@ -484,12 +490,87 @@ export class VectorLayer extends AbstractLayer { } } + async _syncSourceFormatters(syncContext) { + if (this._style.constructor.type !== LAYER_STYLE_TYPE.VECTOR) { + return; + } + + return this._syncFormatters({ + source: this._source, + dataRequestId: SOURCE_FORMATTERS_ID_ORIGIN, + fields: this._style + .getDynamicPropertiesArray() + .filter(dynamicStyleProp => { + return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE; + }) + .map(dynamicStyleProp => { + return dynamicStyleProp.getField(); + }), + ...syncContext, + }); + } + + async _syncJoinFormatters(syncContext, join) { + const joinSource = join.getRightJoinSource(); + return this._syncFormatters({ + source: joinSource, + dataRequestId: join.getSourceFormattersDataRequestId(), + fields: this._style + .getDynamicPropertiesArray() + .filter(dynamicStyleProp => { + const matchingField = joinSource.getMetricFieldForName( + dynamicStyleProp.getField().getName() + ); + return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN && !!matchingField; + }) + .map(dynamicStyleProp => { + return dynamicStyleProp.getField(); + }), + ...syncContext, + }); + } + + async _syncFormatters({ source, dataRequestId, fields, startLoading, stopLoading, onLoadError }) { + if (fields.length === 0) { + return; + } + + const fieldNames = fields.map(field => { + return field.getName(); + }); + const nextMeta = { + fieldNames: _.uniq(fieldNames).sort(), + }; + const prevDataRequest = this._findDataRequestById(dataRequestId); + const canSkipUpdate = canSkipFormattersUpdate({ prevDataRequest, nextMeta }); + if (canSkipUpdate) { + return; + } + + const requestToken = Symbol(`layer-${this.getId()}-${dataRequestId}`); + try { + startLoading(dataRequestId, requestToken, nextMeta); + + const formatters = {}; + const promises = fields.map(async field => { + const fieldName = field.getName(); + formatters[fieldName] = await source.getFieldFormatter(fieldName); + }); + await Promise.all(promises); + + stopLoading(dataRequestId, requestToken, formatters, nextMeta); + } catch (error) { + onLoadError(dataRequestId, requestToken, error.message); + } + } + async syncData(syncContext) { if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { return; } await this._syncSourceStyleMeta(syncContext); + await this._syncSourceFormatters(syncContext); const sourceResult = await this._syncSource(syncContext); if ( !sourceResult.featureCollection || From 27b6e1c4795044cbd67080643a3df5224c55b28d Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Wed, 18 Dec 2019 16:04:12 +0100 Subject: [PATCH 05/61] Upgrade typescript-eslint to 2.12 (#53477) --- package.json | 4 +-- packages/eslint-config-kibana/package.json | 4 +-- yarn.lock | 40 +++++++++++----------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 985fbfd1b6a2f..a771a130d08b1 100644 --- a/package.json +++ b/package.json @@ -365,8 +365,8 @@ "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^2.10.0", - "@typescript-eslint/parser": "^2.10.0", + "@typescript-eslint/eslint-plugin": "^2.12.0", + "@typescript-eslint/parser": "^2.12.0", "angular-mocks": "^1.7.8", "archiver": "^3.1.1", "axe-core": "^3.3.2", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index 7917297883b03..04602d196a7f3 100644 --- a/packages/eslint-config-kibana/package.json +++ b/packages/eslint-config-kibana/package.json @@ -15,8 +15,8 @@ }, "homepage": "https://github.com/elastic/eslint-config-kibana#readme", "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^2.10.0", - "@typescript-eslint/parser": "^2.10.0", + "@typescript-eslint/eslint-plugin": "^2.12.0", + "@typescript-eslint/parser": "^2.12.0", "babel-eslint": "^10.0.3", "eslint": "^6.5.1", "eslint-plugin-babel": "^5.3.0", diff --git a/yarn.lock b/yarn.lock index 7ffd19d49bf55..ef35e06a52f7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4454,24 +4454,24 @@ resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== -"@typescript-eslint/eslint-plugin@^2.10.0": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.10.0.tgz#c4cb103275e555e8a7e9b3d14c5951eb6d431e70" - integrity sha512-rT51fNLW0u3fnDGnAHVC5nu+Das+y2CpW10yqvf6/j5xbuUV3FxA3mBaIbM24CXODXjbgUznNb4Kg9XZOUxKAw== +"@typescript-eslint/eslint-plugin@^2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.12.0.tgz#0da7cbca7b24f4c6919e9eb31c704bfb126f90ad" + integrity sha512-1t4r9rpLuEwl3hgt90jY18wJHSyb0E3orVL3DaqwmpiSDHmHiSspVsvsFF78BJ/3NNG3qmeso836jpuBWYziAA== dependencies: - "@typescript-eslint/experimental-utils" "2.10.0" + "@typescript-eslint/experimental-utils" "2.12.0" eslint-utils "^1.4.3" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.10.0": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.10.0.tgz#8db1656cdfd3d9dcbdbf360b8274dea76f0b2c2c" - integrity sha512-FZhWq6hWWZBP76aZ7bkrfzTMP31CCefVIImrwP3giPLcoXocmLTmr92NLZxuIcTL4GTEOE33jQMWy9PwelL+yQ== +"@typescript-eslint/experimental-utils@2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.12.0.tgz#e0a76ffb6293e058748408a191921e453c31d40d" + integrity sha512-jv4gYpw5N5BrWF3ntROvCuLe1IjRenLy5+U57J24NbPGwZFAjhnM45qpq0nDH1y/AZMb3Br25YiNVwyPbz6RkA== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.10.0" + "@typescript-eslint/typescript-estree" "2.12.0" eslint-scope "^5.0.0" "@typescript-eslint/experimental-utils@^1.13.0": @@ -4483,14 +4483,14 @@ "@typescript-eslint/typescript-estree" "1.13.0" eslint-scope "^4.0.0" -"@typescript-eslint/parser@^2.10.0": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.10.0.tgz#24b2e48384ab6d5a6121e4c4faf8892c79657ad3" - integrity sha512-wQNiBokcP5ZsTuB+i4BlmVWq6o+oAhd8en2eSm/EE9m7BgZUIfEeYFd6z3S+T7bgNuloeiHA1/cevvbBDLr98g== +"@typescript-eslint/parser@^2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.12.0.tgz#393f1604943a4ca570bb1a45bc8834e9b9158884" + integrity sha512-lPdkwpdzxEfjI8TyTzZqPatkrswLSVu4bqUgnB03fHSOwpC7KSerPgJRgIAf11UGNf7HKjJV6oaPZI4AghLU6g== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.10.0" - "@typescript-eslint/typescript-estree" "2.10.0" + "@typescript-eslint/experimental-utils" "2.12.0" + "@typescript-eslint/typescript-estree" "2.12.0" eslint-visitor-keys "^1.1.0" "@typescript-eslint/typescript-estree@1.13.0": @@ -4501,10 +4501,10 @@ lodash.unescape "4.0.1" semver "5.5.0" -"@typescript-eslint/typescript-estree@2.10.0": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.10.0.tgz#89cdabd5e8c774e9d590588cb42fb9afd14dcbd9" - integrity sha512-oOYnplddQNm/LGVkqbkAwx4TIBuuZ36cAQq9v3nFIU9FmhemHuVzAesMSXNQDdAzCa5bFgCrfD3JWhYVKlRN2g== +"@typescript-eslint/typescript-estree@2.12.0": + version "2.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.12.0.tgz#bd9e547ccffd17dfab0c3ab0947c80c8e2eb914c" + integrity sha512-rGehVfjHEn8Frh9UW02ZZIfJs6SIIxIu/K1bbci8rFfDE/1lQ8krIJy5OXOV3DVnNdDPtoiPOdEANkLMrwXbiQ== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" From aa1af60ea4b8bf84dee80ec3c5bf36c3a9e614a0 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Wed, 18 Dec 2019 15:34:58 +0000 Subject: [PATCH 06/61] [SIEM] Alerts view - adding alerts table (#51959) * add alert view to hosts page * add defaultHeaders * add alerts table * fix dsl query * add alerts histogram * add i18n for alerts table * fix types error * fix type issue * whitespace cleanup * fix types * fix types * fix types * fix types * fix types * rename params * fix unit test * fix types * revert change on updateHostsSort * remove unused prop * update unit test * pair programming with angela to get filter working * update alerts query * clean up * fix queries * align type for pageFilters * apply page filter for network page * simplify filter props for alerts view * clean up * replace hard coded tab name --- .../components/alerts_viewer/alerts_table.tsx | 85 +++++++++++++ .../alerts_viewer/default_headers.ts | 68 ++++++++++ .../public/components/alerts_viewer/index.tsx | 59 +++++++++ .../components/alerts_viewer/translations.ts | 19 +++ .../public/components/alerts_viewer/types.ts | 26 ++++ .../public/components/events_viewer/index.tsx | 12 +- .../navigation/breadcrumbs/index.test.ts | 32 ++++- .../navigation/breadcrumbs/index.ts | 48 ++++--- .../page/hosts/alerts_over_time/index.tsx | 30 +++++ .../hosts/alerts_over_time/translation.ts | 24 ++++ .../public/components/url_state/index.tsx | 5 +- .../alerts_over_time.gql_query.ts | 37 ++++++ .../alerts/alerts_over_time/index.tsx | 108 ++++++++++++++++ .../siem/public/graphql/introspection.json | 106 +++++++++++++++- .../plugins/siem/public/graphql/types.ts | 73 ++++++++++- .../plugins/siem/public/mock/global_state.ts | 6 + .../pages/hosts/details/details_tabs.tsx | 6 + .../siem/public/pages/hosts/details/index.tsx | 53 ++++---- .../public/pages/hosts/details/nav_tabs.tsx | 7 ++ .../siem/public/pages/hosts/details/types.ts | 4 +- .../siem/public/pages/hosts/details/utils.ts | 14 ++- .../plugins/siem/public/pages/hosts/hosts.tsx | 13 +- .../siem/public/pages/hosts/hosts_tabs.tsx | 5 + .../plugins/siem/public/pages/hosts/index.tsx | 6 +- .../siem/public/pages/hosts/nav_tabs.tsx | 7 ++ .../navigation/alerts_query_tab_body.tsx | 54 ++++++++ .../public/pages/hosts/navigation/index.ts | 1 + .../public/pages/hosts/navigation/types.ts | 15 ++- .../siem/public/pages/hosts/translations.ts | 4 + .../public/pages/network/ip_details/utils.ts | 42 +++++-- .../navigation/alerts_query_tab_body.tsx | 68 ++++++++++ .../pages/network/navigation/nav_tabs.tsx | 7 ++ .../network/navigation/network_routes.tsx | 5 + .../public/pages/network/navigation/types.ts | 7 +- .../public/pages/network/navigation/utils.ts | 5 +- .../siem/public/pages/network/network.tsx | 17 ++- .../siem/public/pages/network/translations.ts | 4 + .../siem/public/store/hosts/helpers.test.ts | 16 +++ .../siem/public/store/hosts/helpers.ts | 8 ++ .../plugins/siem/public/store/hosts/model.ts | 2 + .../siem/public/store/hosts/reducer.ts | 8 ++ .../siem/public/store/hosts/selectors.ts | 3 + .../siem/public/store/network/helpers.test.ts | 8 ++ .../siem/public/store/network/model.ts | 2 + .../siem/public/store/network/reducer.ts | 4 + .../plugins/siem/public/utils/route/types.ts | 12 +- .../siem/server/graphql/alerts/index.ts | 8 ++ .../siem/server/graphql/alerts/resolvers.ts | 38 ++++++ .../siem/server/graphql/alerts/schema.gql.ts | 23 ++++ .../plugins/siem/server/graphql/index.ts | 2 + .../plugins/siem/server/graphql/types.ts | 83 ++++++++++-- .../legacy/plugins/siem/server/init_server.ts | 2 + .../lib/alerts/elasticsearch_adapter.ts | 63 ++++++++++ .../lib/alerts/elasticseatch_adapter.test.ts | 57 +++++++++ .../plugins/siem/server/lib/alerts/index.ts | 21 ++++ .../plugins/siem/server/lib/alerts/mock.ts | 115 +++++++++++++++++ .../siem/server/lib/alerts/query.dsl.ts | 118 ++++++++++++++++++ .../plugins/siem/server/lib/alerts/types.ts | 27 ++++ .../plugins/siem/server/lib/compose/kibana.ts | 2 + .../lib/events/elasticsearch_adapter.ts | 1 - .../legacy/plugins/siem/server/lib/types.ts | 2 + 61 files changed, 1608 insertions(+), 99 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/alerts_viewer/default_headers.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/page/hosts/alerts_over_time/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/page/hosts/alerts_over_time/translation.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/alerts/alerts_over_time/alerts_over_time.gql_query.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/alerts/alerts_over_time/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx create mode 100644 x-pack/legacy/plugins/siem/server/graphql/alerts/index.ts create mode 100644 x-pack/legacy/plugins/siem/server/graphql/alerts/resolvers.ts create mode 100644 x-pack/legacy/plugins/siem/server/graphql/alerts/schema.gql.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/alerts/elasticsearch_adapter.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/alerts/index.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/alerts/mock.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/alerts/query.dsl.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/alerts/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx new file mode 100644 index 0000000000000..e0101dc3ab74b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 } from 'react'; + +import { esFilters } from '../../../../../../../src/plugins/data/common/es_query'; +import { StatefulEventsViewer } from '../events_viewer'; +import * as i18n from './translations'; +import { alertsDefaultModel } from './default_headers'; + +export interface OwnProps { + end: number; + id: string; + start: number; +} + +const ALERTS_TABLE_ID = 'timeline-alerts-table'; +const defaultAlertsFilters: esFilters.Filter[] = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'event.kind', + params: { + query: 'alert', + }, + }, + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'event.kind': 'alert', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + }, +]; + +export const AlertsTable = React.memo( + ({ + endDate, + startDate, + pageFilters = [], + }: { + endDate: number; + startDate: number; + pageFilters?: esFilters.Filter[]; + }) => { + const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); + return ( + ({ + documentType: i18n.ALERTS_DOCUMENT_TYPE, + footerText: i18n.TOTAL_COUNT_OF_ALERTS, + showCheckboxes: false, + showRowRenderers: false, + title: i18n.ALERTS_TABLE_TITLE, + }), + [] + )} + /> + ); + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/default_headers.ts b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/default_headers.ts new file mode 100644 index 0000000000000..52990f521b58d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/default_headers.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 { ColumnHeader } from '../timeline/body/column_headers/column_header'; +import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; +import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../timeline/body/helpers'; +import { timelineDefaults, SubsetTimelineModel } from '../../store/timeline/model'; + +export const alertsHeaders: ColumnHeader[] = [ + { + columnHeaderType: defaultColumnHeaderType, + id: '@timestamp', + width: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.module', + width: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.dataset', + width: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.category', + width: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'event.severity', + width: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'observer.name', + width: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'host.name', + width: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'message', + width: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'agent.id', + width: DEFAULT_COLUMN_MIN_WIDTH, + }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'agent.type', + width: DEFAULT_COLUMN_MIN_WIDTH, + }, +]; + +export const alertsDefaultModel: SubsetTimelineModel = { + ...timelineDefaults, + columns: alertsHeaders, +}; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx new file mode 100644 index 0000000000000..c8f1bb2278917 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.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 { noop } from 'lodash/fp'; +import React from 'react'; + +import { EuiSpacer } from '@elastic/eui'; +import { manageQuery } from '../page/manage_query'; +import { AlertsOverTimeHistogram } from '../page/hosts/alerts_over_time'; +import { AlertsComponentsQueryProps } from './types'; +import { AlertsOverTimeQuery } from '../../containers/alerts/alerts_over_time'; +import { hostsModel } from '../../store/model'; +import { AlertsTable } from './alerts_table'; + +const AlertsOverTimeManage = manageQuery(AlertsOverTimeHistogram); +export const AlertsView = ({ + defaultFilters, + deleteQuery, + endDate, + filterQuery, + pageFilters, + skip, + setQuery, + startDate, + type, + updateDateRange = noop, +}: AlertsComponentsQueryProps) => ( + <> + + {({ alertsOverTime, loading, id, inspect, refetch, totalCount }) => ( + + )} + + + + +); + +AlertsView.displayName = 'AlertsView'; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts new file mode 100644 index 0000000000000..987665c9413e3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.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 { i18n } from '@kbn/i18n'; + +export const ALERTS_DOCUMENT_TYPE = i18n.translate('xpack.siem.hosts.alertsDocumentType', { + defaultMessage: 'Alerts', +}); + +export const TOTAL_COUNT_OF_ALERTS = i18n.translate('xpack.siem.hosts.totalCountOfAlerts', { + defaultMessage: 'alerts match the search criteria', +}); + +export const ALERTS_TABLE_TITLE = i18n.translate('xpack.siem.hosts.alertsDocumentType', { + defaultMessage: 'Alerts', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts new file mode 100644 index 0000000000000..8a17c1102e776 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts @@ -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 { esFilters } from '../../../../../../../src/plugins/data/common'; +import { HostsComponentsQueryProps } from '../../pages/hosts/navigation/types'; +import { NetworkComponentQueryProps } from '../../pages/network/navigation/types'; + +type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; +export interface AlertsComponentsQueryProps + extends Pick< + CommonQueryProps, + | 'deleteQuery' + | 'endDate' + | 'filterQuery' + | 'skip' + | 'setQuery' + | 'startDate' + | 'type' + | 'updateDateRange' + > { + pageFilters: esFilters.Filter[]; + defaultFilters?: esFilters.Filter[]; +} diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx index 21292e4ac3254..b614776cd90cf 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx @@ -5,7 +5,7 @@ */ import { isEqual } from 'lodash/fp'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import chrome from 'ui/chrome'; @@ -32,6 +32,7 @@ export interface OwnProps { id: string; start: number; headerFilterGroup?: React.ReactNode; + pageFilters?: esFilters.Filter[]; timelineTypeContext?: TimelineTypeContextProps; utilityBar?: (totalCount: number) => React.ReactNode; } @@ -150,7 +151,7 @@ const StatefulEventsViewerComponent = React.memo( const handleOnMouseEnter = useCallback(() => setShowInspect(true), []); const handleOnMouseLeave = useCallback(() => setShowInspect(false), []); - + const eventsFilter = useMemo(() => [...filters], [defaultFilters]); return (
( id={id} dataProviders={dataProviders!} end={end} - filters={[...filters, ...defaultFilters]} + filters={eventsFilter} headerFilterGroup={headerFilterGroup} indexPattern={indexPatterns ?? { fields: [], title: '' }} isLive={isLive} @@ -192,7 +193,8 @@ const StatefulEventsViewerComponent = React.memo( isEqual(prevProps.query, nextProps.query) && prevProps.pageCount === nextProps.pageCount && isEqual(prevProps.sort, nextProps.sort) && - prevProps.start === nextProps.start + prevProps.start === nextProps.start && + isEqual(prevProps.defaultFilters, nextProps.defaultFilters) ); StatefulEventsViewerComponent.displayName = 'StatefulEventsViewerComponent'; @@ -227,7 +229,7 @@ export const StatefulEventsViewer = connect(makeMapStateToProps, { createTimeline: timelineActions.createTimeline, deleteEventQuery: inputsActions.deleteOneQuery, updateItemsPerPage: timelineActions.updateItemsPerPage, - updateSort: timelineActions.updateSort, removeColumn: timelineActions.removeColumn, upsertColumn: timelineActions.upsertColumn, + setSearchBarFilter: inputsActions.setSearchBarFilter, })(StatefulEventsViewerComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts index 02135348957ff..f9d63a8594180 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts @@ -9,8 +9,9 @@ import { encodeIpv6 } from '../../../lib/helpers'; import { getBreadcrumbsForRoute, setBreadcrumbs } from '.'; import { HostsTableType } from '../../../store/hosts/model'; -import { RouteSpyState } from '../../../utils/route/types'; +import { RouteSpyState, SiemRouteType } from '../../../utils/route/types'; import { TabNavigationProps } from '../tab_navigation/types'; +import { NetworkRouteType } from '../../../pages/network/navigation/types'; jest.mock('ui/chrome', () => ({ getBasePath: () => { @@ -30,6 +31,17 @@ jest.mock('../../search_bar', () => ({ }, })); +const mockDefaultTab = (pageName: string): SiemRouteType | undefined => { + switch (pageName) { + case 'hosts': + return HostsTableType.authentications; + case 'network': + return NetworkRouteType.flows; + default: + return undefined; + } +}; + const getMockObject = ( pageName: string, pathName: string, @@ -69,7 +81,7 @@ const getMockObject = ( pageName, pathName, search: '', - tabName: HostsTableType.authentications, + tabName: mockDefaultTab(pageName) as HostsTableType, query: { query: '', language: 'kuery' }, filters: [], timeline: { @@ -136,6 +148,10 @@ describe('Navigation Breadcrumbs', () => { href: '#/link-to/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', }, + { + text: 'Flows', + href: '', + }, ]); }); @@ -176,7 +192,11 @@ describe('Navigation Breadcrumbs', () => { href: '#/link-to/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', }, - { text: '192.0.2.255', href: '' }, + { + text: ipv4, + href: `#/link-to/network/ip/${ipv4}?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, + }, + { text: 'Flows', href: '' }, ]); }); @@ -189,7 +209,11 @@ describe('Navigation Breadcrumbs', () => { href: '#/link-to/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', }, - { text: '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff', href: '' }, + { + text: ipv6, + href: `#/link-to/network/ip/${ipv6Encoded}?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, + }, + { text: 'Flows', href: '' }, ]); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts index 8d9ebb964ce63..9eee5b21e83f3 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -6,20 +6,20 @@ import chrome, { Breadcrumb } from 'ui/chrome'; -import { getOr } from 'lodash/fp'; +import { getOr, omit } from 'lodash/fp'; import { APP_NAME } from '../../../../common/constants'; import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; import { SiemPageName } from '../../../pages/home/types'; -import { RouteSpyState } from '../../../utils/route/types'; +import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState } from '../../../utils/route/types'; import { getOverviewUrl } from '../../link_to'; import { TabNavigationProps } from '../tab_navigation/types'; import { getSearch } from '../helpers'; import { SearchNavTab } from '../types'; -export const setBreadcrumbs = (object: RouteSpyState & TabNavigationProps) => { - const breadcrumbs = getBreadcrumbsForRoute(object); +export const setBreadcrumbs = (spyState: RouteSpyState & TabNavigationProps) => { + const breadcrumbs = getBreadcrumbsForRoute(spyState); if (breadcrumbs) { chrome.breadcrumbs.set(breadcrumbs); } @@ -32,19 +32,26 @@ export const siemRootBreadcrumb: Breadcrumb[] = [ }, ]; +const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => + spyState != null && spyState.pageName === SiemPageName.network; + +const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => + spyState != null && spyState.pageName === SiemPageName.hosts; + export const getBreadcrumbsForRoute = ( object: RouteSpyState & TabNavigationProps ): Breadcrumb[] | null => { - if (object != null && object.navTabs && object.pageName === SiemPageName.hosts) { + const spyState: RouteSpyState = omit('navTabs', object); + if (isHostsRoutes(spyState) && object.navTabs) { const tempNav: SearchNavTab = { urlKey: 'host', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, object.pageName, object.navTabs)]; - if (object.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, object.tabName, object.navTabs)]; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; } return [ ...siemRootBreadcrumb, ...getHostDetailsBreadcrumbs( - object, + spyState, urlStateKeys.reduce( (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], [] @@ -52,22 +59,33 @@ export const getBreadcrumbsForRoute = ( ), ]; } - if (object != null && object.navTabs && object.pageName === SiemPageName.network) { + if (isNetworkRoutes(spyState) && object.navTabs) { const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; - const urlStateKeys = [getOr(tempNav, object.pageName, object.navTabs)]; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } return [ ...siemRootBreadcrumb, ...getIPDetailsBreadcrumbs( - object.detailName, - urlStateKeys.reduce((acc: string[], item) => [...acc, getSearch(item, object)], []) + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) ), ]; } - if (object != null && object.navTabs && object.pageName && object.navTabs[object.pageName]) { + if ( + spyState != null && + object.navTabs && + spyState.pageName && + object.navTabs[spyState.pageName] + ) { return [ ...siemRootBreadcrumb, { - text: object.navTabs[object.pageName].name, + text: object.navTabs[spyState.pageName].name, href: '', }, ]; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/alerts_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/alerts_over_time/index.tsx new file mode 100644 index 0000000000000..031e1cd767be8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/alerts_over_time/index.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 * as i18n from './translation'; +import { MatrixHistogram } from '../../../matrix_histogram'; +import { MatrixHistogramBasicProps } from '../../../matrix_histogram/types'; +import { MatrixOverTimeHistogramData } from '../../../../graphql/types'; + +export const AlertsOverTimeHistogram = ( + props: MatrixHistogramBasicProps +) => { + const dataKey = 'alertsOverTime'; + const { totalCount } = props; + const subtitle = `${i18n.SHOWING}: ${totalCount.toLocaleString()} ${i18n.UNIT(totalCount)}`; + const { ...matrixOverTimeProps } = props; + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/alerts_over_time/translation.ts b/x-pack/legacy/plugins/siem/public/components/page/hosts/alerts_over_time/translation.ts new file mode 100644 index 0000000000000..380ca0cd3baaf --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/alerts_over_time/translation.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 { i18n } from '@kbn/i18n'; + +export const ALERTS_COUNT_FREQUENCY_BY_MODULE = i18n.translate( + 'xpack.siem.alertsOverTime.alertsCountFrequencyByModuleTitle', + { + defaultMessage: 'Alerts count by module', + } +); + +export const SHOWING = i18n.translate('xpack.siem.alertsOverTime.showing', { + defaultMessage: 'Showing', +}); + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.siem.alertsOverTime.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {alert} other {alerts}}`, + }); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx index 8164348620b50..a7e7729de2e27 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx @@ -43,6 +43,9 @@ export const UrlStateRedux = compose(props => { const [routeProps] = useRouteSpy(); - const urlStateReduxProps: RouteSpyState & UrlStateProps = { ...routeProps, ...props }; + const urlStateReduxProps: RouteSpyState & UrlStateProps = { + ...routeProps, + ...props, + }; return ; }); diff --git a/x-pack/legacy/plugins/siem/public/containers/alerts/alerts_over_time/alerts_over_time.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/alerts/alerts_over_time/alerts_over_time.gql_query.ts new file mode 100644 index 0000000000000..428cf25ea1b8e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/alerts/alerts_over_time/alerts_over_time.gql_query.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 gql from 'graphql-tag'; + +export const AlertsOverTimeGqlQuery = gql` + query GetAlertsOverTimeQuery( + $sourceId: ID! + $timerange: TimerangeInput! + $defaultIndex: [String!]! + $filterQuery: String + $inspect: Boolean! + ) { + source(id: $sourceId) { + id + AlertsHistogram( + timerange: $timerange + filterQuery: $filterQuery + defaultIndex: $defaultIndex + ) { + alertsOverTimeByModule { + x + y + g + } + totalCount + inspect @include(if: $inspect) { + dsl + response + } + } + } + } +`; diff --git a/x-pack/legacy/plugins/siem/public/containers/alerts/alerts_over_time/index.tsx b/x-pack/legacy/plugins/siem/public/containers/alerts/alerts_over_time/index.tsx new file mode 100644 index 0000000000000..98dcef51292ae --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/alerts/alerts_over_time/index.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; + +import chrome from 'ui/chrome'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { inputsModel, State, inputsSelectors, hostsModel } from '../../../store'; +import { createFilter, getDefaultFetchPolicy } from '../../helpers'; +import { QueryTemplate, QueryTemplateProps } from '../../query_template'; + +import { AlertsOverTimeGqlQuery } from './alerts_over_time.gql_query'; +import { MatrixOverTimeHistogramData, GetAlertsOverTimeQuery } from '../../../graphql/types'; + +const ID = 'alertsOverTimeQuery'; + +export interface AlertsArgs { + endDate: number; + alertsOverTime: MatrixOverTimeHistogramData[]; + id: string; + inspect: inputsModel.InspectQuery; + loading: boolean; + refetch: inputsModel.Refetch; + startDate: number; + totalCount: number; +} + +export interface OwnProps extends QueryTemplateProps { + children?: (args: AlertsArgs) => React.ReactNode; + type: hostsModel.HostsType; +} + +export interface AlertsOverTimeComponentReduxProps { + isInspected: boolean; +} + +type AlertsOverTimeProps = OwnProps & AlertsOverTimeComponentReduxProps; + +class AlertsOverTimeComponentQuery extends QueryTemplate< + AlertsOverTimeProps, + GetAlertsOverTimeQuery.Query, + GetAlertsOverTimeQuery.Variables +> { + public render() { + const { + children, + endDate, + filterQuery, + id = ID, + isInspected, + sourceId, + startDate, + } = this.props; + return ( + + query={AlertsOverTimeGqlQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + variables={{ + filterQuery: createFilter(filterQuery), + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const source = getOr({}, `source.AlertsHistogram`, data); + const alertsOverTime = getOr([], `alertsOverTimeByModule`, source); + const totalCount = getOr(-1, 'totalCount', source); + return children!({ + endDate: endDate!, + alertsOverTime, + id, + inspect: getOr(null, 'inspect', source), + loading, + refetch, + startDate: startDate!, + totalCount, + }); + }} + + ); + } +} + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +export const AlertsOverTimeQuery = connect(makeMapStateToProps)(AlertsOverTimeComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 7c173a9a90626..8ebc66b7f38a7 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -666,6 +666,53 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "AlertsHistogram", + "description": "", + "args": [ + { + "name": "filterQuery", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "defaultIndex", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "defaultValue": null + }, + { + "name": "timerange", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "AlertsOverTimeData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "AnomaliesOverTime", "description": "", @@ -2540,7 +2587,7 @@ }, { "kind": "OBJECT", - "name": "AnomaliesOverTimeData", + "name": "AlertsOverTimeData", "description": "", "fields": [ { @@ -2552,7 +2599,7 @@ "deprecationReason": null }, { - "name": "anomaliesOverTime", + "name": "alertsOverTimeByModule", "description": "", "args": [], "type": { @@ -2691,6 +2738,61 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "AnomaliesOverTimeData", + "description": "", + "fields": [ + { + "name": "inspect", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "anomaliesOverTime", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MatrixOverTimeHistogramData", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "PaginationInputPaginated", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index 1464b55648035..6dfde08058f7c 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -457,6 +457,8 @@ export interface Source { /** The status of the source */ status: SourceStatus; + AlertsHistogram: AlertsOverTimeData; + AnomaliesOverTime: AnomaliesOverTimeData; /** Gets Authentication success and failures based on a timerange */ Authentications: AuthenticationsData; @@ -558,10 +560,10 @@ export interface IndexField { format?: Maybe; } -export interface AnomaliesOverTimeData { +export interface AlertsOverTimeData { inspect?: Maybe; - anomaliesOverTime: MatrixOverTimeHistogramData[]; + alertsOverTimeByModule: MatrixOverTimeHistogramData[]; totalCount: number; } @@ -580,6 +582,14 @@ export interface MatrixOverTimeHistogramData { g: string; } +export interface AnomaliesOverTimeData { + inspect?: Maybe; + + anomaliesOverTime: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + export interface AuthenticationsData { edges: AuthenticationsEdges[]; @@ -2137,6 +2147,13 @@ export interface GetAllTimelineQueryArgs { onlyUserFavorite?: Maybe; } +export interface AlertsHistogramSourceArgs { + filterQuery?: Maybe; + + defaultIndex: string[]; + + timerange: TimerangeInput; +} export interface AnomaliesOverTimeSourceArgs { timerange: TimerangeInput; @@ -2438,6 +2455,58 @@ export interface DeleteTimelineMutationArgs { // Documents // ==================================================== +export namespace GetAlertsOverTimeQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + defaultIndex: string[]; + filterQuery?: Maybe; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + AlertsHistogram: AlertsHistogram; + }; + + export type AlertsHistogram = { + __typename?: 'AlertsOverTimeData'; + + alertsOverTimeByModule: AlertsOverTimeByModule[]; + + totalCount: number; + + inspect: Maybe; + }; + + export type AlertsOverTimeByModule = { + __typename?: 'MatrixOverTimeHistogramData'; + + x: number; + + y: number; + + g: string; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + export namespace GetAnomaliesOverTimeQuery { export type Variables = { sourceId: string; diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/legacy/plugins/siem/public/mock/global_state.ts index ada34acbc1946..750d5292950be 100644 --- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts +++ b/x-pack/legacy/plugins/siem/public/mock/global_state.ts @@ -45,6 +45,7 @@ export const mockGlobalState: State = { events: { activePage: 0, limit: 10 }, uncommonProcesses: { activePage: 0, limit: 10 }, anomalies: null, + alerts: { activePage: 0, limit: 10 }, }, }, details: { @@ -59,6 +60,7 @@ export const mockGlobalState: State = { events: { activePage: 0, limit: 10 }, uncommonProcesses: { activePage: 0, limit: 10 }, anomalies: null, + alerts: { activePage: 0, limit: 10 }, }, }, }, @@ -101,6 +103,10 @@ export const mockGlobalState: State = { limit: 10, sort: { direction: Direction.desc }, }, + [networkModel.NetworkTableType.alerts]: { + activePage: 0, + limit: 10, + }, }, }, details: { diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx index a09e21f2d1a35..5774feb46240d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx @@ -21,10 +21,12 @@ import { AuthenticationsQueryTabBody, UncommonProcessQueryTabBody, EventsQueryTabBody, + HostAlertsQueryTabBody, } from '../navigation'; const HostDetailsTabs = React.memo( ({ + pageFilters, deleteQuery, filterQuery, from, @@ -93,6 +95,10 @@ const HostDetailsTabs = React.memo( path={`${hostDetailsPagePath}/:tabName(${HostsTableType.events})`} render={() => } /> + } + /> ); } diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx index d8bcc6fe3c294..e062e65bde496 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx @@ -34,7 +34,7 @@ import { inputsSelectors, State } from '../../../store'; import { setHostDetailsTablesActivePageToZero as dispatchHostDetailsTablesActivePageToZero } from '../../../store/hosts/actions'; import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; import { SpyRoute } from '../../../utils/route/spy_routes'; -import { esQuery } from '../../../../../../../../src/plugins/data/public'; +import { esQuery, esFilters } from '../../../../../../../../src/plugins/data/public'; import { HostsEmptyPage } from '../hosts_empty_page'; import { HostDetailsTabs } from './details_tabs'; @@ -64,6 +64,30 @@ const HostDetailsComponent = React.memo( }, [setHostDetailsTablesActivePageToZero, detailName]); const capabilities = useContext(MlCapabilitiesContext); const core = useKibanaCore(); + const hostDetailsPageFilters: esFilters.Filter[] = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.name', + value: detailName, + params: { + query: detailName, + }, + }, + query: { + match: { + 'host.name': { + query: detailName, + type: 'phrase', + }, + }, + }, + }, + ]; + const getFilters = () => [...hostDetailsPageFilters, ...filters]; const narrowDateRange = useCallback( (min: number, max: number) => { setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); @@ -79,32 +103,8 @@ const HostDetailsComponent = React.memo( config: esQuery.getEsQueryConfig(core.uiSettings), indexPattern, queries: [query], - filters: [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.name', - value: detailName, - params: { - query: detailName, - }, - }, - query: { - match: { - 'host.name': { - query: detailName, - type: 'phrase', - }, - }, - }, - }, - ...filters, - ], + filters: getFilters(), }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( @@ -198,6 +198,7 @@ const HostDetailsComponent = React.memo( ; export type HostDetailsTabsProps = HostBodyComponentDispatchProps & HostsQueryProps & { + pageFilters?: esFilters.Filter[]; + filterQuery: string; indexPattern: IIndexPattern; type: hostsModel.HostsType; - filterQuery: string; }; export type SetAbsoluteRangeDatePicker = ActionCreator<{ diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts index 7483636cfe03d..52e016502940b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts @@ -6,30 +6,33 @@ import { Breadcrumb } from 'ui/chrome'; +import { get } from 'lodash/fp'; import { hostsModel } from '../../../store'; import { HostsTableType } from '../../../store/hosts/model'; import { getHostsUrl, getHostDetailsUrl } from '../../../components/link_to/redirect_to_hosts'; import * as i18n from '../translations'; -import { RouteSpyState } from '../../../utils/route/types'; +import { HostRouteSpyState } from '../../../utils/route/types'; export const type = hostsModel.HostsType.details; -const TabNameMappedToI18nKey = { +const TabNameMappedToI18nKey: Record = { [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, [HostsTableType.authentications]: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, [HostsTableType.uncommonProcesses]: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, + [HostsTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, }; -export const getBreadcrumbs = (params: RouteSpyState, search: string[]): Breadcrumb[] => { +export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): Breadcrumb[] => { let breadcrumb = [ { text: i18n.PAGE_TITLE, href: `${getHostsUrl()}${search && search[0] ? search[0] : ''}`, }, ]; + if (params.detailName != null) { breadcrumb = [ ...breadcrumb, @@ -40,10 +43,13 @@ export const getBreadcrumbs = (params: RouteSpyState, search: string[]): Breadcr ]; } if (params.tabName != null) { + const tabName = get('tabName', params); + if (!tabName) return breadcrumb; + breadcrumb = [ ...breadcrumb, { - text: TabNameMappedToI18nKey[params.tabName], + text: TabNameMappedToI18nKey[tabName], href: '', }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx index 0c058f25854c0..6d217a9301884 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx @@ -10,6 +10,7 @@ import { connect } from 'react-redux'; import { StickyContainer } from 'react-sticky'; import { compose } from 'redux'; +import { useParams } from 'react-router-dom'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; import { LastEventTime } from '../../components/last_event_time'; @@ -35,6 +36,8 @@ import { HostsTabs } from './hosts_tabs'; import { navTabsHosts } from './nav_tabs'; import * as i18n from './translations'; import { HostsComponentProps, HostsComponentReduxProps } from './types'; +import { filterAlertsHosts } from './navigation'; +import { HostsTableType } from '../../store/hosts/model'; const KpiHostsComponentManage = manageQuery(KpiHostsComponent); @@ -52,6 +55,14 @@ const HostsComponent = React.memo( }) => { const capabilities = React.useContext(MlCapabilitiesContext); const core = useKibanaCore(); + const { tabName } = useParams(); + + const hostsFilters = React.useMemo(() => { + if (tabName === HostsTableType.alerts) { + return filters.length > 0 ? [...filters, ...filterAlertsHosts] : filterAlertsHosts; + } + return filters; + }, [tabName]); const narrowDateRange = useCallback( (min: number, max: number) => { setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); @@ -67,7 +78,7 @@ const HostsComponent = React.memo( config: esQuery.getEsQueryConfig(core.uiSettings), indexPattern, queries: [query], - filters, + filters: hostsFilters, }); return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx index d6e76deb276bd..9c13fc4ac386e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx @@ -20,6 +20,7 @@ import { UncommonProcessQueryTabBody, EventsQueryTabBody, } from './navigation'; +import { HostAlertsQueryTabBody } from './navigation/alerts_query_tab_body'; const HostsTabs = memo( ({ @@ -80,6 +81,10 @@ const HostsTabs = memo( path={`${hostsPagePath}/:tabName(${HostsTableType.events})`} render={() => } /> + } + /> ); } diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx index c8d450a62cc57..fff5c5218c003 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx @@ -21,14 +21,16 @@ const getHostsTabPath = (pagePath: string) => `${HostsTableType.authentications}|` + `${HostsTableType.uncommonProcesses}|` + `${HostsTableType.anomalies}|` + - `${HostsTableType.events})`; + `${HostsTableType.events}|` + + `${HostsTableType.alerts})`; const getHostDetailsTabPath = (pagePath: string) => `${hostDetailsPagePath}/:tabName(` + `${HostsTableType.authentications}|` + `${HostsTableType.uncommonProcesses}|` + `${HostsTableType.anomalies}|` + - `${HostsTableType.events})`; + `${HostsTableType.events}|` + + `${HostsTableType.alerts})`; type Props = Partial> & { url: string }; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/nav_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/nav_tabs.tsx index 0756efe1f9b6e..4109feff099e0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/nav_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/nav_tabs.tsx @@ -49,6 +49,13 @@ export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { disabled: false, urlKey: 'host', }, + [HostsTableType.alerts]: { + id: HostsTableType.alerts, + name: i18n.NAVIGATION_ALERTS_TITLE, + href: getTabsOnHostsUrl(HostsTableType.alerts), + disabled: false, + urlKey: 'host', + }, }; return hasMlUserPermissions ? hostsNavTabs : omit([HostsTableType.anomalies], hostsNavTabs); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx new file mode 100644 index 0000000000000..b893acd4dbb3b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.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 React, { useMemo } from 'react'; + +import { esFilters } from '../../../../../../../../src/plugins/data/common/es_query'; +import { AlertsView } from '../../../components/alerts_viewer'; +import { AlertsComponentQueryProps } from './types'; + +export const filterAlertsHosts: esFilters.Filter[] = [ + { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + exists: { + field: 'host.name', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + meta: { + alias: '', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"query": {"bool": {"filter": [{"bool": {"should": [{"exists": {"field": "host.name"}}],"minimum_should_match": 1}}]}}}', + }, + }, +]; +export const HostAlertsQueryTabBody = React.memo((alertsProps: AlertsComponentQueryProps) => { + const { pageFilters, ...rest } = alertsProps; + const hostPageFilters = useMemo( + () => (pageFilters != null ? [...filterAlertsHosts, ...pageFilters] : filterAlertsHosts), + [pageFilters] + ); + + return ; +}); + +HostAlertsQueryTabBody.displayName = 'HostAlertsQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts index f20138f520620..a93c4dfcbcef4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts @@ -8,3 +8,4 @@ export * from './authentications_query_tab_body'; export * from './events_query_tab_body'; export * from './hosts_query_tab_body'; export * from './uncommon_process_query_tab_body'; +export * from './alerts_query_tab_body'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts index cfe7953f16cee..107b35edc7f7a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IIndexPattern } from 'src/plugins/data/public'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/'; import { NarrowDateRange } from '../../../components/ml/types'; -import { hostsModel } from '../../../store'; import { ESTermQuery } from '../../../../common/typed_json'; import { InspectQuery, Refetch } from '../../../store/inputs/model'; -import { HostsTableType } from '../../../store/hosts/model'; +import { HostsTableType, HostsType } from '../../../store/hosts/model'; import { NavTab } from '../../../components/navigation/types'; import { UpdateDateRange } from '../../../components/charts/common'; +import { esFilters } from '../../../../../../../../src/plugins/data/common/es_query'; export type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts & HostsTableType.authentications & @@ -37,8 +37,8 @@ export type SetQuery = ({ refetch: Refetch; }) => void; -interface QueryTabBodyProps { - type: hostsModel.HostsType; +export interface QueryTabBodyProps { + type: HostsType; startDate: number; endDate: number; filterQuery?: string | ESTermQuery; @@ -53,4 +53,9 @@ export type HostsComponentsQueryProps = QueryTabBodyProps & { narrowDateRange?: NarrowDateRange; }; +export type AlertsComponentQueryProps = HostsComponentsQueryProps & { + filterQuery: string; + pageFilters?: esFilters.Filter[]; +}; + export type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/translations.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/translations.ts index 1c95cbed71a4a..87617f6bc5f7f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/translations.ts @@ -46,6 +46,10 @@ export const NAVIGATION_EVENTS_TITLE = i18n.translate('xpack.siem.hosts.navigati defaultMessage: 'Events', }); +export const NAVIGATION_ALERTS_TITLE = i18n.translate('xpack.siem.hosts.navigation.alertsTitle', { + defaultMessage: 'Alerts', +}); + export const EMPTY_TITLE = i18n.translate('xpack.siem.hosts.emptyTitle', { defaultMessage: 'It looks like you don’t have any indices relevant to hosts in the SIEM application', diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts index 222bf108b4fad..c265fb30a2439 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts @@ -6,30 +6,50 @@ import { Breadcrumb } from 'ui/chrome'; +import { get } from 'lodash/fp'; import { decodeIpv6 } from '../../../lib/helpers'; -import { getNetworkUrl } from '../../../components/link_to/redirect_to_network'; +import { getNetworkUrl, getIPDetailsUrl } from '../../../components/link_to/redirect_to_network'; import { networkModel } from '../../../store/network'; import * as i18n from '../translations'; +import { NetworkRouteType } from '../navigation/types'; +import { NetworkRouteSpyState } from '../../../utils/route/types'; export const type = networkModel.NetworkType.details; +const TabNameMappedToI18nKey: Record = { + [NetworkRouteType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, + [NetworkRouteType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, + [NetworkRouteType.flows]: i18n.NAVIGATION_FLOWS_TITLE, + [NetworkRouteType.dns]: i18n.NAVIGATION_DNS_TITLE, + [NetworkRouteType.http]: i18n.NAVIGATION_HTTP_TITLE, + [NetworkRouteType.tls]: i18n.NAVIGATION_TLS_TITLE, +}; -export const getBreadcrumbs = (ip: string | undefined, search: string[]): Breadcrumb[] => { - const breadcrumbs = [ +export const getBreadcrumbs = (params: NetworkRouteSpyState, search: string[]): Breadcrumb[] => { + let breadcrumb = [ { text: i18n.PAGE_TITLE, href: `${getNetworkUrl()}${search && search[0] ? search[0] : ''}`, }, ]; - - if (ip) { - return [ - ...breadcrumbs, + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, { - text: decodeIpv6(ip), - href: '', + text: decodeIpv6(params.detailName), + href: `${getIPDetailsUrl(params.detailName)}${search && search[1] ? search[1] : ''}`, }, ]; - } else { - return breadcrumbs; } + + const tabName = get('tabName', params); + if (!tabName) return breadcrumb; + + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[tabName], + href: '', + }, + ]; + return breadcrumb; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx new file mode 100644 index 0000000000000..3eeabd3007afa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { esFilters } from '../../../../../../../../src/plugins/data/common/es_query'; +import { AlertsView } from '../../../components/alerts_viewer'; +import { NetworkComponentQueryProps } from './types'; + +export const filterAlertsNetwork: esFilters.Filter[] = [ + { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + bool: { + should: [ + { + exists: { + field: 'source.ip', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + exists: { + field: 'destination.ip', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + meta: { + alias: '', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"exists":{"field": "source.ip"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field": "destination.ip"}}],"minimum_should_match":1}}],"minimum_should_match":1}}]}}', + }, + }, +]; + +export const NetworkAlertsQueryTabBody = React.memo((alertsProps: NetworkComponentQueryProps) => ( + +)); + +NetworkAlertsQueryTabBody.displayName = 'NetworkAlertsQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/nav_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/nav_tabs.tsx index fbf137df39872..61f1a5aacb9c0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/nav_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/nav_tabs.tsx @@ -47,6 +47,13 @@ export const navTabsNetwork = (hasMlUserPermissions: boolean): NetworkNavTab => disabled: false, urlKey: 'network', }, + [NetworkRouteType.alerts]: { + id: NetworkRouteType.alerts, + name: i18n.NAVIGATION_ALERTS_TITLE, + href: getTabsOnNetworkUrl(NetworkRouteType.alerts), + disabled: false, + urlKey: 'network', + }, }; return hasMlUserPermissions ? networkNavTabs : omit([NetworkRouteType.anomalies], networkNavTabs); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx index d1c792ade0985..acc5d02299f1f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx @@ -21,6 +21,7 @@ import { ConditionalFlexGroup } from './conditional_flex_group'; import { NetworkRoutesProps, NetworkRouteType } from './types'; import { TlsQueryTabBody } from './tls_query_tab_body'; import { Anomaly } from '../../../components/ml/types'; +import { NetworkAlertsQueryTabBody } from './alerts_query_tab_body'; export const NetworkRoutes = ({ networkPagePath, @@ -143,6 +144,10 @@ export const NetworkRoutes = ({ /> )} /> + } + /> ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts index b8ad8877bf3c1..b6063a81f31f6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IIndexPattern } from 'src/plugins/data/public'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/'; import { NavTab } from '../../../components/navigation/types'; import { FlowTargetSourceDest } from '../../../graphql/types'; @@ -52,7 +52,9 @@ export type NetworkRoutesProps = GlobalTimeArgs & { export type KeyNetworkNavTabWithoutMlPermission = NetworkRouteType.dns & NetworkRouteType.flows & - NetworkRouteType.tls; + NetworkRouteType.http & + NetworkRouteType.tls & + NetworkRouteType.alerts; type KeyNetworkNavTabWithMlPermission = KeyNetworkNavTabWithoutMlPermission & NetworkRouteType.anomalies; @@ -67,6 +69,7 @@ export enum NetworkRouteType { anomalies = 'anomalies', tls = 'tls', http = 'http', + alerts = 'alerts', } export type GetNetworkRoutePath = ( diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts index 059949bf51837..24c2011fd3800 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts @@ -12,7 +12,7 @@ export const getNetworkRoutePath: GetNetworkRoutePath = ( hasMlUserPermission ) => { if (capabilitiesFetched && !hasMlUserPermission) { - return `${pagePath}/:tabName(${NetworkRouteType.flows}|${NetworkRouteType.dns}|${NetworkRouteType.http}|${NetworkRouteType.tls})`; + return `${pagePath}/:tabName(${NetworkRouteType.flows}|${NetworkRouteType.dns}|${NetworkRouteType.http}|${NetworkRouteType.tls}|${NetworkRouteType.alerts})`; } return ( @@ -21,6 +21,7 @@ export const getNetworkRoutePath: GetNetworkRoutePath = ( `${NetworkRouteType.dns}|` + `${NetworkRouteType.anomalies}|` + `${NetworkRouteType.http}|` + - `${NetworkRouteType.tls})` + `${NetworkRouteType.tls}|` + + `${NetworkRouteType.alerts})` ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network.tsx index 116664fef6ddc..0d8d3a6753c59 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network.tsx @@ -5,10 +5,12 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { connect } from 'react-redux'; +import { useParams } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; +import { esQuery } from '../../../../../../../src/plugins/data/public'; import { EmbeddedMap } from '../../components/embeddables/embedded_map'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; @@ -27,10 +29,11 @@ import { networkModel, State, inputsSelectors } from '../../store'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { SpyRoute } from '../../utils/route/spy_routes'; import { navTabsNetwork, NetworkRoutes, NetworkRoutesLoading } from './navigation'; +import { filterAlertsNetwork } from './navigation/alerts_query_tab_body'; import { NetworkEmptyPage } from './network_empty_page'; import * as i18n from './translations'; import { NetworkComponentProps } from './types'; -import { esQuery } from '../../../../../../../src/plugins/data/public'; +import { NetworkRouteType } from './navigation/types'; const KpiNetworkComponentManage = manageQuery(KpiNetworkComponent); const sourceId = 'default'; @@ -49,6 +52,14 @@ const NetworkComponent = React.memo( capabilitiesFetched, }) => { const core = useKibanaCore(); + const { tabName } = useParams(); + + const networkFilters = useMemo(() => { + if (tabName === NetworkRouteType.alerts) { + return filters.length > 0 ? [...filters, ...filterAlertsNetwork] : filterAlertsNetwork; + } + return filters; + }, [tabName]); const narrowDateRange = useCallback( (min: number, max: number) => { setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); @@ -64,7 +75,7 @@ const NetworkComponent = React.memo( config: esQuery.getEsQueryConfig(core.uiSettings), indexPattern, queries: [query], - filters, + filters: networkFilters, }); return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( diff --git a/x-pack/legacy/plugins/siem/public/pages/network/translations.ts b/x-pack/legacy/plugins/siem/public/pages/network/translations.ts index be222bf5f2531..91c3338ff7903 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/translations.ts @@ -49,3 +49,7 @@ export const NAVIGATION_ANOMALIES_TITLE = i18n.translate( defaultMessage: 'Anomalies', } ); + +export const NAVIGATION_ALERTS_TITLE = i18n.translate('xpack.siem.network.navigation.alertsTitle', { + defaultMessage: 'Alerts', +}); diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/helpers.test.ts b/x-pack/legacy/plugins/siem/public/store/hosts/helpers.test.ts index 8721121295aad..a4eddb31b3e31 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/helpers.test.ts @@ -31,6 +31,10 @@ export const mockHostsState: HostsModel = { limit: DEFAULT_TABLE_LIMIT, }, [HostsTableType.anomalies]: null, + [HostsTableType.alerts]: { + activePage: 4, + limit: DEFAULT_TABLE_LIMIT, + }, }, }, details: { @@ -54,6 +58,10 @@ export const mockHostsState: HostsModel = { limit: DEFAULT_TABLE_LIMIT, }, [HostsTableType.anomalies]: null, + [HostsTableType.alerts]: { + activePage: 4, + limit: DEFAULT_TABLE_LIMIT, + }, }, }, }; @@ -81,6 +89,10 @@ describe('Hosts redux store', () => { activePage: 0, limit: 10, }, + alerts: { + activePage: 0, + limit: 10, + }, }); }); @@ -105,6 +117,10 @@ describe('Hosts redux store', () => { activePage: 0, limit: 10, }, + alerts: { + activePage: 0, + limit: 10, + }, }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/helpers.ts b/x-pack/legacy/plugins/siem/public/store/hosts/helpers.ts index 3e32dde465401..f6b5596b382f6 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/helpers.ts @@ -26,6 +26,10 @@ export const setHostPageQueriesActivePageToZero = (state: HostsModel): Queries = ...state.page.queries[HostsTableType.uncommonProcesses], activePage: DEFAULT_TABLE_ACTIVE_PAGE, }, + [HostsTableType.alerts]: { + ...state.page.queries[HostsTableType.alerts], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, }); export const setHostDetailsQueriesActivePageToZero = (state: HostsModel): Queries => ({ @@ -46,6 +50,10 @@ export const setHostDetailsQueriesActivePageToZero = (state: HostsModel): Querie ...state.details.queries[HostsTableType.uncommonProcesses], activePage: DEFAULT_TABLE_ACTIVE_PAGE, }, + [HostsTableType.alerts]: { + ...state.page.queries[HostsTableType.alerts], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, }); export const setHostsQueriesActivePageToZero = (state: HostsModel, type: HostsType): Queries => { diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/model.ts b/x-pack/legacy/plugins/siem/public/store/hosts/model.ts index 8b21537292207..b3764224633b8 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/model.ts @@ -17,6 +17,7 @@ export enum HostsTableType { events = 'events', uncommonProcesses = 'uncommonProcesses', anomalies = 'anomalies', + alerts = 'alerts', } export interface BasicQueryPaginated { @@ -35,6 +36,7 @@ export interface Queries { [HostsTableType.events]: BasicQueryPaginated; [HostsTableType.uncommonProcesses]: BasicQueryPaginated; [HostsTableType.anomalies]: null | undefined; + [HostsTableType.alerts]: BasicQueryPaginated; } export interface GenericHostsModel { diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts b/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts index 11b0a985c5762..53fe9a3ea6a2c 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts @@ -46,6 +46,10 @@ export const initialHostsState: HostsState = { limit: DEFAULT_TABLE_LIMIT, }, [HostsTableType.anomalies]: null, + [HostsTableType.alerts]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, }, }, details: { @@ -69,6 +73,10 @@ export const initialHostsState: HostsState = { limit: DEFAULT_TABLE_LIMIT, }, [HostsTableType.anomalies]: null, + [HostsTableType.alerts]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, }, }, }; diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts b/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts index 8ebeae4bba392..e50968db31f60 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts @@ -24,3 +24,6 @@ export const eventsSelector = () => createSelector(selectHosts, hosts => hosts.q export const uncommonProcessesSelector = () => createSelector(selectHosts, hosts => hosts.queries.uncommonProcesses); + +export const alertsSelector = () => + createSelector(selectHosts, hosts => hosts.queries[HostsTableType.alerts]); diff --git a/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts b/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts index a15e187b95e60..933c2f05a57ba 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts @@ -73,6 +73,10 @@ export const mockNetworkState: NetworkModel = { limit: DEFAULT_TABLE_LIMIT, sort: { direction: Direction.desc }, }, + [NetworkTableType.alerts]: { + activePage: 0, + limit: DEFAULT_TABLE_LIMIT, + }, }, }, details: { @@ -186,6 +190,10 @@ describe('Network redux store', () => { field: 'bytes_out', }, }, + [NetworkTableType.alerts]: { + activePage: 0, + limit: 10, + }, }); }); diff --git a/x-pack/legacy/plugins/siem/public/store/network/model.ts b/x-pack/legacy/plugins/siem/public/store/network/model.ts index 45c49d6598881..4ddfb84024970 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/model.ts @@ -19,6 +19,7 @@ export enum NetworkType { } export enum NetworkTableType { + alerts = 'alerts', dns = 'dns', http = 'http', topCountriesDestination = 'topCountriesDestination', @@ -103,6 +104,7 @@ export interface NetworkQueries { [NetworkTableType.topNFlowDestination]: TopNFlowQuery; [NetworkTableType.topNFlowSource]: TopNFlowQuery; [NetworkTableType.tls]: TlsQuery; + [NetworkTableType.alerts]: BasicQueryPaginated; } export interface NetworkPageModel { diff --git a/x-pack/legacy/plugins/siem/public/store/network/reducer.ts b/x-pack/legacy/plugins/siem/public/store/network/reducer.ts index 373bedb63c3e7..8e4d4555d3bd9 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/reducer.ts @@ -89,6 +89,10 @@ export const initialNetworkState: NetworkState = { direction: Direction.desc, }, }, + [NetworkTableType.alerts]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, }, }, details: { diff --git a/x-pack/legacy/plugins/siem/public/utils/route/types.ts b/x-pack/legacy/plugins/siem/public/utils/route/types.ts index 62f6b67df245f..002cd4d23786d 100644 --- a/x-pack/legacy/plugins/siem/public/utils/route/types.ts +++ b/x-pack/legacy/plugins/siem/public/utils/route/types.ts @@ -9,16 +9,26 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { HostsTableType } from '../../store/hosts/model'; +import { NetworkRouteType } from '../../pages/network/navigation/types'; +export type SiemRouteType = HostsTableType | NetworkRouteType; export interface RouteSpyState { pageName: string; detailName: string | undefined; - tabName: HostsTableType | undefined; + tabName: SiemRouteType | undefined; search: string; pathName: string; history?: H.History; } +export interface HostRouteSpyState extends RouteSpyState { + tabName: HostsTableType | undefined; +} + +export interface NetworkRouteSpyState extends RouteSpyState { + tabName: NetworkRouteType | undefined; +} + export type RouteSpyAction = | { type: 'updateSearch'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/alerts/index.ts b/x-pack/legacy/plugins/siem/server/graphql/alerts/index.ts new file mode 100644 index 0000000000000..f2beae525ed6b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/alerts/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 { createAlertsResolvers } from './resolvers'; +export { alertsSchema } from './schema.gql'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/alerts/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/alerts/resolvers.ts new file mode 100644 index 0000000000000..3becaa4d169d9 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/alerts/resolvers.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 { Alerts } from '../../lib/alerts'; +import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; +import { createOptions } from '../../utils/build_query/create_options'; +import { QuerySourceResolver } from '../sources/resolvers'; +import { SourceResolvers } from '../types'; + +export interface AlertsResolversDeps { + alerts: Alerts; +} + +type QueryAlertsHistogramResolver = ChildResolverOf< + AppResolverOf, + QuerySourceResolver +>; + +export const createAlertsResolvers = ( + libs: AlertsResolversDeps +): { + Source: { + AlertsHistogram: QueryAlertsHistogramResolver; + }; +} => ({ + Source: { + async AlertsHistogram(source, args, { req }, info) { + const options = { + ...createOptions(source, args, info), + defaultIndex: args.defaultIndex, + }; + return libs.alerts.getAlertsHistogramData(req, options); + }, + }, +}); diff --git a/x-pack/legacy/plugins/siem/server/graphql/alerts/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/alerts/schema.gql.ts new file mode 100644 index 0000000000000..f29b64772b8f6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/alerts/schema.gql.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 gql from 'graphql-tag'; + +export const alertsSchema = gql` + type AlertsOverTimeData { + inspect: Inspect + alertsOverTimeByModule: [MatrixOverTimeHistogramData!]! + totalCount: Float! + } + + extend type Source { + AlertsHistogram( + filterQuery: String + defaultIndex: [String!]! + timerange: TimerangeInput! + ): AlertsOverTimeData! + } +`; diff --git a/x-pack/legacy/plugins/siem/server/graphql/index.ts b/x-pack/legacy/plugins/siem/server/graphql/index.ts index 4e28605f6c0b2..762b9002a466d 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/index.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/index.ts @@ -29,7 +29,9 @@ import { timelineSchema } from './timeline'; import { tlsSchema } from './tls'; import { uncommonProcessesSchema } from './uncommon_processes'; import { whoAmISchema } from './who_am_i'; +import { alertsSchema } from './alerts'; export const schemas = [ + alertsSchema, anomaliesSchema, authenticationsSchema, ecsSchema, diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index fda79ad543bf6..776444b1502b1 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -459,6 +459,8 @@ export interface Source { /** The status of the source */ status: SourceStatus; + AlertsHistogram: AlertsOverTimeData; + AnomaliesOverTime: AnomaliesOverTimeData; /** Gets Authentication success and failures based on a timerange */ Authentications: AuthenticationsData; @@ -560,10 +562,10 @@ export interface IndexField { format?: Maybe; } -export interface AnomaliesOverTimeData { +export interface AlertsOverTimeData { inspect?: Maybe; - anomaliesOverTime: MatrixOverTimeHistogramData[]; + alertsOverTimeByModule: MatrixOverTimeHistogramData[]; totalCount: number; } @@ -582,6 +584,14 @@ export interface MatrixOverTimeHistogramData { g: string; } +export interface AnomaliesOverTimeData { + inspect?: Maybe; + + anomaliesOverTime: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + export interface AuthenticationsData { edges: AuthenticationsEdges[]; @@ -2139,6 +2149,13 @@ export interface GetAllTimelineQueryArgs { onlyUserFavorite?: Maybe; } +export interface AlertsHistogramSourceArgs { + filterQuery?: Maybe; + + defaultIndex: string[]; + + timerange: TimerangeInput; +} export interface AnomaliesOverTimeSourceArgs { timerange: TimerangeInput; @@ -2781,6 +2798,8 @@ export namespace SourceResolvers { /** The status of the source */ status?: StatusResolver; + AlertsHistogram?: AlertsHistogramResolver; + AnomaliesOverTime?: AnomaliesOverTimeResolver; /** Gets Authentication success and failures based on a timerange */ Authentications?: AuthenticationsResolver; @@ -2853,6 +2872,19 @@ export namespace SourceResolvers { Parent, TContext >; + export type AlertsHistogramResolver< + R = AlertsOverTimeData, + Parent = Source, + TContext = SiemContext + > = Resolver; + export interface AlertsHistogramArgs { + filterQuery?: Maybe; + + defaultIndex: string[]; + + timerange: TimerangeInput; + } + export type AnomaliesOverTimeResolver< R = AnomaliesOverTimeData, Parent = Source, @@ -3407,11 +3439,11 @@ export namespace IndexFieldResolvers { > = Resolver; } -export namespace AnomaliesOverTimeDataResolvers { - export interface Resolvers { +export namespace AlertsOverTimeDataResolvers { + export interface Resolvers { inspect?: InspectResolver, TypeParent, TContext>; - anomaliesOverTime?: AnomaliesOverTimeResolver< + alertsOverTimeByModule?: AlertsOverTimeByModuleResolver< MatrixOverTimeHistogramData[], TypeParent, TContext @@ -3422,17 +3454,17 @@ export namespace AnomaliesOverTimeDataResolvers { export type InspectResolver< R = Maybe, - Parent = AnomaliesOverTimeData, + Parent = AlertsOverTimeData, TContext = SiemContext > = Resolver; - export type AnomaliesOverTimeResolver< + export type AlertsOverTimeByModuleResolver< R = MatrixOverTimeHistogramData[], - Parent = AnomaliesOverTimeData, + Parent = AlertsOverTimeData, TContext = SiemContext > = Resolver; export type TotalCountResolver< R = number, - Parent = AnomaliesOverTimeData, + Parent = AlertsOverTimeData, TContext = SiemContext > = Resolver; } @@ -3482,6 +3514,36 @@ export namespace MatrixOverTimeHistogramDataResolvers { > = Resolver; } +export namespace AnomaliesOverTimeDataResolvers { + export interface Resolvers { + inspect?: InspectResolver, TypeParent, TContext>; + + anomaliesOverTime?: AnomaliesOverTimeResolver< + MatrixOverTimeHistogramData[], + TypeParent, + TContext + >; + + totalCount?: TotalCountResolver; + } + + export type InspectResolver< + R = Maybe, + Parent = AnomaliesOverTimeData, + TContext = SiemContext + > = Resolver; + export type AnomaliesOverTimeResolver< + R = MatrixOverTimeHistogramData[], + Parent = AnomaliesOverTimeData, + TContext = SiemContext + > = Resolver; + export type TotalCountResolver< + R = number, + Parent = AnomaliesOverTimeData, + TContext = SiemContext + > = Resolver; +} + export namespace AuthenticationsDataResolvers { export interface Resolvers { edges?: EdgesResolver; @@ -8707,9 +8769,10 @@ export type IResolvers = { SourceFields?: SourceFieldsResolvers.Resolvers; SourceStatus?: SourceStatusResolvers.Resolvers; IndexField?: IndexFieldResolvers.Resolvers; - AnomaliesOverTimeData?: AnomaliesOverTimeDataResolvers.Resolvers; + AlertsOverTimeData?: AlertsOverTimeDataResolvers.Resolvers; Inspect?: InspectResolvers.Resolvers; MatrixOverTimeHistogramData?: MatrixOverTimeHistogramDataResolvers.Resolvers; + AnomaliesOverTimeData?: AnomaliesOverTimeDataResolvers.Resolvers; AuthenticationsData?: AuthenticationsDataResolvers.Resolvers; AuthenticationsEdges?: AuthenticationsEdgesResolvers.Resolvers; AuthenticationItem?: AuthenticationItemResolvers.Resolvers; diff --git a/x-pack/legacy/plugins/siem/server/init_server.ts b/x-pack/legacy/plugins/siem/server/init_server.ts index 08c481164d539..5ecbb51c6770d 100644 --- a/x-pack/legacy/plugins/siem/server/init_server.ts +++ b/x-pack/legacy/plugins/siem/server/init_server.ts @@ -29,10 +29,12 @@ import { createUncommonProcessesResolvers } from './graphql/uncommon_processes'; import { createWhoAmIResolvers } from './graphql/who_am_i'; import { AppBackendLibs } from './lib/types'; import { createTlsResolvers } from './graphql/tls'; +import { createAlertsResolvers } from './graphql/alerts'; export const initServer = (libs: AppBackendLibs) => { const schema = makeExecutableSchema({ resolvers: [ + createAlertsResolvers(libs) as IResolvers, createAnomaliesResolvers(libs) as IResolvers, createAuthenticationsResolvers(libs) as IResolvers, createEsValueResolvers() as IResolvers, diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/elasticsearch_adapter.ts new file mode 100644 index 0000000000000..6667f34b1b738 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/alerts/elasticsearch_adapter.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 { get, getOr } from 'lodash/fp'; + +import { AlertsOverTimeData, MatrixOverTimeHistogramData } from '../../graphql/types'; + +import { inspectStringifyObject } from '../../utils/build_query'; + +import { FrameworkAdapter, FrameworkRequest, RequestBasicOptions } from '../framework'; +import { buildAlertsHistogramQuery } from './query.dsl'; + +import { AlertsAdapter, AlertsGroupData, AlertsBucket } from './types'; +import { TermAggregation } from '../types'; +import { EventHit } from '../events/types'; + +export class ElasticsearchAlertsAdapter implements AlertsAdapter { + constructor(private readonly framework: FrameworkAdapter) {} + + public async getAlertsHistogramData( + request: FrameworkRequest, + options: RequestBasicOptions + ): Promise { + const dsl = buildAlertsHistogramQuery(options); + const response = await this.framework.callWithRequest( + request, + 'search', + dsl + ); + const totalCount = getOr(0, 'hits.total.value', response); + const alertsOverTimeByModule = getOr([], 'aggregations.alertsByModuleGroup.buckets', response); + const inspect = { + dsl: [inspectStringifyObject(dsl)], + response: [inspectStringifyObject(response)], + }; + return { + inspect, + alertsOverTimeByModule: getAlertsOverTimeByModule(alertsOverTimeByModule), + totalCount, + }; + } +} + +const getAlertsOverTimeByModule = (data: AlertsGroupData[]): MatrixOverTimeHistogramData[] => { + let result: MatrixOverTimeHistogramData[] = []; + data.forEach(({ key: group, alerts }) => { + const alertsData: AlertsBucket[] = get('buckets', alerts); + + result = [ + ...result, + ...alertsData.map(({ key, doc_count }: AlertsBucket) => ({ + x: key, + y: doc_count, + g: group, + })), + ]; + }); + + return result; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.ts new file mode 100644 index 0000000000000..a24fb5f511d24 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/alerts/elasticseatch_adapter.test.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 { FrameworkAdapter, FrameworkRequest, RequestBasicOptions } from '../framework'; + +import expect from '@kbn/expect'; +import { ElasticsearchAlertsAdapter } from './elasticsearch_adapter'; +import { + mockRequest, + mockOptions, + mockAlertsHistogramDataResponse, + mockAlertsHistogramQueryDsl, + mockAlertsHistogramDataFormattedResponse, +} from './mock'; + +jest.mock('./query.dsl', () => { + return { + buildAlertsHistogramQuery: jest.fn(() => mockAlertsHistogramQueryDsl), + }; +}); + +describe('alerts elasticsearch_adapter', () => { + describe('getAlertsHistogramData', () => { + test('Happy Path ', async () => { + const mockCallWithRequest = jest.fn(); + mockCallWithRequest.mockImplementation((req: FrameworkRequest, method: string) => { + return mockAlertsHistogramDataResponse; + }); + const mockFramework: FrameworkAdapter = { + version: 'mock', + callWithRequest: mockCallWithRequest, + registerGraphQLEndpoint: jest.fn(), + getIndexPatternsService: jest.fn(), + }; + jest.doMock('../framework', () => ({ + callWithRequest: mockCallWithRequest, + })); + + const EsNetworkTimelineAlerts = new ElasticsearchAlertsAdapter(mockFramework); + const data = await EsNetworkTimelineAlerts.getAlertsHistogramData( + (mockRequest as unknown) as FrameworkRequest, + (mockOptions as unknown) as RequestBasicOptions + ); + + expect(data).to.eql({ + alertsOverTimeByModule: mockAlertsHistogramDataFormattedResponse, + inspect: { + dsl: ['"mockAlertsHistogramQueryDsl"'], + response: [JSON.stringify(mockAlertsHistogramDataResponse, null, 2)], + }, + totalCount: 1599508, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/index.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/index.ts new file mode 100644 index 0000000000000..13a693a6e1fbb --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/alerts/index.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 { FrameworkRequest, RequestBasicOptions } from '../framework'; +export * from './elasticsearch_adapter'; +import { AlertsAdapter } from './types'; +import { AlertsOverTimeData } from '../../graphql/types'; + +export class Alerts { + constructor(private readonly adapter: AlertsAdapter) {} + + public async getAlertsHistogramData( + req: FrameworkRequest, + options: RequestBasicOptions + ): Promise { + return this.adapter.getAlertsHistogramData(req, options); + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/mock.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/mock.ts new file mode 100644 index 0000000000000..fe0b6673f3191 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/alerts/mock.ts @@ -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 { defaultIndexPattern } from '../../../default_index_pattern'; + +export const mockAlertsHistogramDataResponse = { + took: 513, + timed_out: false, + _shards: { + total: 62, + successful: 61, + skipped: 0, + failed: 1, + failures: [ + { + shard: 0, + index: 'auditbeat-7.2.0', + node: 'jBC5kcOeT1exvECDMrk5Ug', + reason: { + type: 'illegal_argument_exception', + reason: + 'Fielddata is disabled on text fields by default. Set fielddata=true on [event.module] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.', + }, + }, + ], + }, + hits: { + total: { + value: 1599508, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + alertsByModuleGroup: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 802087, + buckets: [ + { + key: 'All others', + doc_count: 451519, + alerts: { + buckets: [ + { + key_as_string: '2019-12-15T09:30:00.000Z', + key: 1576402200000, + doc_count: 3008, + }, + { + key_as_string: '2019-12-15T10:00:00.000Z', + key: 1576404000000, + doc_count: 8671, + }, + ], + }, + }, + { + key: 'suricata', + doc_count: 345902, + alerts: { + buckets: [ + { + key_as_string: '2019-12-15T09:30:00.000Z', + key: 1576402200000, + doc_count: 1785, + }, + { + key_as_string: '2019-12-15T10:00:00.000Z', + key: 1576404000000, + doc_count: 5342, + }, + ], + }, + }, + ], + }, + }, +}; +export const mockAlertsHistogramDataFormattedResponse = [ + { + x: 1576402200000, + y: 3008, + g: 'All others', + }, + { + x: 1576404000000, + y: 8671, + g: 'All others', + }, + { + x: 1576402200000, + y: 1785, + g: 'suricata', + }, + { + x: 1576404000000, + y: 5342, + g: 'suricata', + }, +]; +export const mockAlertsHistogramQueryDsl = 'mockAlertsHistogramQueryDsl'; +export const mockRequest = 'mockRequest'; +export const mockOptions = { + sourceConfiguration: { field: {} }, + timerange: { + to: 9999, + from: 1234, + }, + defaultIndex: defaultIndexPattern, + filterQuery: '', +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/query.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/query.dsl.ts new file mode 100644 index 0000000000000..efa6ee01f2124 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/alerts/query.dsl.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 { createQueryFilterClauses, calculateTimeseriesInterval } from '../../utils/build_query'; +import { buildTimelineQuery } from '../events/query.dsl'; +import { RequestOptions, RequestBasicOptions } from '../framework'; + +export const buildAlertsQuery = (options: RequestOptions) => { + const eventsQuery = buildTimelineQuery(options); + const eventsFilter = eventsQuery.body.query.bool.filter; + const alertsFilter = [ + ...createQueryFilterClauses({ match: { 'event.kind': { query: 'alert' } } }), + ]; + + return { + ...eventsQuery, + body: { + ...eventsQuery.body, + query: { + bool: { + filter: [...eventsFilter, ...alertsFilter], + }, + }, + }, + }; +}; + +export const buildAlertsHistogramQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, + sourceConfiguration: { + fields: { timestamp }, + }, +}: RequestBasicOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'event.kind': 'alert', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + range: { + [timestamp]: { + gte: from, + lte: to, + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeseriesInterval(from, to); + const histogramTimestampField = '@timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: `${interval}s`, + }, + }; + const autoDateHistogram = { + auto_date_histogram: { + field: histogramTimestampField, + buckets: 36, + }, + }; + return { + alertsByModuleGroup: { + terms: { + field: 'event.module', + missing: 'All others', + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + alerts: interval ? dateHistogram : autoDateHistogram, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/alerts/types.ts new file mode 100644 index 0000000000000..e6a4ff4b7c9d1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/alerts/types.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 { AlertsOverTimeData } from '../../graphql/types'; +import { FrameworkRequest, RequestBasicOptions } from '../framework'; + +export interface AlertsBucket { + key: number; + doc_count: number; +} + +export interface AlertsGroupData { + key: string; + doc_count: number; + alerts: { + buckets: AlertsBucket[]; + }; +} +export interface AlertsAdapter { + getAlertsHistogramData( + request: FrameworkRequest, + options: RequestBasicOptions + ): Promise; +} diff --git a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts index 9d5ac6db7bbb7..2e4dfbc31b65b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts @@ -31,6 +31,7 @@ import { ElasticsearchUncommonProcessesAdapter, UncommonProcesses } from '../unc import { Note } from '../note/saved_object'; import { PinnedEvent } from '../pinned_event/saved_object'; import { Timeline } from '../timeline/saved_object'; +import { Alerts, ElasticsearchAlertsAdapter } from '../alerts'; export function compose(core: CoreSetup, env: PluginInitializerContext['env']): AppBackendLibs { const framework = new KibanaBackendFrameworkAdapter(core, env); @@ -42,6 +43,7 @@ export function compose(core: CoreSetup, env: PluginInitializerContext['env']): const pinnedEvent = new PinnedEvent(); const domainLibs: AppDomainLibs = { + alerts: new Alerts(new ElasticsearchAlertsAdapter(framework)), anomalies: new Anomalies(new ElasticsearchAnomaliesAdapter(framework)), authentications: new Authentications(new ElasticsearchAuthenticationAdapter(framework)), events: new Events(new ElasticsearchEventsAdapter(framework)), diff --git a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts index 6dbb75d28149b..dfa81122f9c23 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts @@ -61,7 +61,6 @@ export class ElasticsearchEventsAdapter implements EventsAdapter { ...reduceFields(queryOptions.fields, eventFieldsMap), ]); delete queryOptions.fieldRequested; - const dsl = buildTimelineQuery(queryOptions); const response = await this.framework.callWithRequest( request, diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 9e4e477aa78d2..9034ab4e6af83 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -23,10 +23,12 @@ import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; import { TLS } from './tls'; +import { Alerts } from './alerts'; export * from './hosts'; export interface AppDomainLibs { + alerts: Alerts; anomalies: Anomalies; authentications: Authentications; events: Events; From 3ab4b7f2fd7fbefaa35dd7e66e5d45063d19b748 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 18 Dec 2019 07:38:59 -0800 Subject: [PATCH 07/61] [ML] Keep rule editor flyout open on refresh (#53458) * [ML] prevent AnomaliesTable re-render * [ML] 50935 update titles to sentence case * [ML] update snapshots --- .../rule_editor_flyout.test.js.snap | 8 +++---- .../rule_editor/rule_editor_flyout.js | 6 ++--- .../rule_action_panel.test.js.snap | 24 ++++++++++++++----- .../select_rule_action/rule_action_panel.js | 9 +++++-- .../select_rule_action/select_rule_action.js | 2 +- .../timeseriesexplorer/timeseriesexplorer.js | 8 +++---- 6 files changed, 36 insertions(+), 21 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap index 11dca03c938a2..4486899efb001 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/__snapshots__/rule_editor_flyout.test.js.snap @@ -26,7 +26,7 @@ exports[`RuleEditorFlyout renders the flyout after adding a condition to a rule id="flyoutTitle" > @@ -276,7 +276,7 @@ exports[`RuleEditorFlyout renders the flyout after setting the rule to edit 1`] id="flyoutTitle" > @@ -540,7 +540,7 @@ exports[`RuleEditorFlyout renders the flyout for creating a rule with conditions id="flyoutTitle" > @@ -782,7 +782,7 @@ exports[`RuleEditorFlyout renders the select action component for a detector wit id="flyoutTitle" > diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index 032bb92418c12..232419cc7dca2 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -510,7 +510,7 @@ export const RuleEditorFlyout = injectI18n(

@@ -569,12 +569,12 @@ export const RuleEditorFlyout = injectI18n( {isCreate === true ? ( ) : ( )} diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap index 463d0b5dbed53..d82f78cbc4e1a 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/rule_action_panel.test.js.snap @@ -11,7 +11,7 @@ exports[`RuleActionPanel renders panel for rule with a condition 1`] = ` Object { "description": "skip result when actual is less than 1", "title": , @@ -40,7 +40,11 @@ exports[`RuleActionPanel renders panel for rule with a condition 1`] = ` conditionValue={1} updateConditionValue={[Function]} />, - "title": "actions", + "title": , }, Object { "description": , @@ -90,7 +94,11 @@ exports[`RuleActionPanel renders panel for rule with a condition and scope, valu fieldValue="AAL" filterId="eu-airlines" />, - "title": "actions", + "title": , }, Object { "description": , @@ -144,7 +152,11 @@ exports[`RuleActionPanel renders panel for rule with scope, value in filter list values={Object {}} /> , - "title": "actions", + "title": , }, Object { "description": ), description: buildRuleDescription(this.rule, this.props.anomaly), @@ -186,7 +186,12 @@ export class RuleActionPanel extends Component { }); } - descriptionListItems[1].title = 'actions'; + descriptionListItems[1].title = ( + + ); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/select_rule_action.js b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/select_rule_action.js index 4d7a8f1b67b74..309e271ad26a4 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/select_rule_action.js +++ b/x-pack/legacy/plugins/ml/public/application/components/rule_editor/select_rule_action/select_rule_action.js @@ -59,7 +59,7 @@ export function SelectRuleAction({ diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 62bb2de3fcb31..0f9ef2b54fdc2 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -1530,13 +1530,11 @@ export class TimeSeriesExplorer extends React.Component { - )} + {arePartitioningFieldsProvided && jobs.length > 0 && ( + + )} ); } From 13c2ed4e439520d32e070e2ab52b7b8362264056 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Wed, 18 Dec 2019 09:35:04 -0700 Subject: [PATCH 08/61] [Metrics UI] Customize "node info" section per Inventory Model (#53325) --- .../inventory_models/aws_ec2/layout.tsx | 204 +++--- .../inventory_models/aws_rds/layout.tsx | 321 ++++----- .../common/inventory_models/aws_s3/layout.tsx | 247 +++---- .../inventory_models/aws_sqs/layout.tsx | 247 +++---- .../inventory_models/container/layout.tsx | 402 +++++------ .../common/inventory_models/host/layout.tsx | 668 +++++++++--------- .../common/inventory_models/pod/layout.tsx | 267 +++---- .../metrics/components/layout_content.tsx | 12 + ...{node_details.tsx => metadata_details.tsx} | 66 +- .../metrics/components/node_details_page.tsx | 26 +- .../metrics/containers/metadata_context.ts | 9 + 11 files changed, 1281 insertions(+), 1188 deletions(-) create mode 100644 x-pack/legacy/plugins/infra/public/pages/metrics/components/layout_content.tsx rename x-pack/legacy/plugins/infra/public/pages/metrics/components/{node_details.tsx => metadata_details.tsx} (72%) create mode 100644 x-pack/legacy/plugins/infra/public/pages/metrics/containers/metadata_context.ts diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/layout.tsx index 01009b478951a..a3074b78f9f3b 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/layout.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/layout.tsx @@ -8,109 +8,123 @@ import { i18n } from '@kbn/i18n'; import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; import { Section } from '../../../public/pages/metrics/components/section'; import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; import { withTheme } from '../../../../../common/eui_styled_components'; +import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( -
- + +
- - - - - - - - -
+ + + + + + + + + +
+
)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/layout.tsx index 5f1185666a35d..debb569fcd5bb 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/layout.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/layout.tsx @@ -10,171 +10,174 @@ import { Section } from '../../../public/pages/metrics/components/section'; import { SubSection } from '../../../public/pages/metrics/components/sub_section'; import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; import { withTheme } from '../../../../../common/eui_styled_components'; +import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( -
- +
- - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + +
+
)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/layout.tsx index 80089f15b04b2..955960f5baeda 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/layout.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/layout.tsx @@ -10,134 +10,137 @@ import { Section } from '../../../public/pages/metrics/components/section'; import { SubSection } from '../../../public/pages/metrics/components/sub_section'; import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; import { withTheme } from '../../../../../common/eui_styled_components'; +import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( -
- +
- - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + +
+
)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/layout.tsx index 40cb0a64d83cc..5d460c971ec3b 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/layout.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/layout.tsx @@ -10,134 +10,137 @@ import { Section } from '../../../public/pages/metrics/components/section'; import { SubSection } from '../../../public/pages/metrics/components/sub_section'; import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; import { withTheme } from '../../../../../common/eui_styled_components'; +import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( -
- +
- - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + +
+
)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/container/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/container/layout.tsx index 00da70f1d96a5..e207687cf8643 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/container/layout.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/container/layout.tsx @@ -11,212 +11,220 @@ import { SubSection } from '../../../public/pages/metrics/components/sub_section import { GaugesSectionVis } from '../../../public/pages/metrics/components/gauges_section_vis'; import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; import { withTheme } from '../../../../../common/eui_styled_components'; +import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( -
- - - - + +
- - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + + +
+
)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/host/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/host/layout.tsx index fee79d8364c42..ca53193e64ca2 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/host/layout.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/host/layout.tsx @@ -5,348 +5,366 @@ */ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { withTheme } from '../../../../../common/eui_styled_components/eui_styled_components'; import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; import { Section } from '../../../public/pages/metrics/components/section'; import { SubSection } from '../../../public/pages/metrics/components/sub_section'; import { GaugesSectionVis } from '../../../public/pages/metrics/components/gauges_section_vis'; import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; -import { withTheme } from '../../../../../common/eui_styled_components'; import * as Aws from '../shared/layouts/aws'; import * as Ngnix from '../shared/layouts/nginx'; +import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; +import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( -
- - - - + +
- - - + + + + + + + + + + + + + + +
+
- - - - - - - - -
-
- - - - - - - - - - - - - - - -
- - + + + + + + + + + + + + + + + +
+ + +
)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/pod/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/pod/layout.tsx index 401e25c4defb8..f0c27ccff13b1 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/pod/layout.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/pod/layout.tsx @@ -12,143 +12,148 @@ import { GaugesSectionVis } from '../../../public/pages/metrics/components/gauge import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; import { withTheme } from '../../../../../common/eui_styled_components'; import * as Nginx from '../shared/layouts/nginx'; +import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; +import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( -
- - - - + +
- - - - - - - - -
- + + + + + + + + + + + + +
+ +
)); diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/layout_content.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/layout_content.tsx new file mode 100644 index 0000000000000..a2bd9cdc13179 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/layout_content.tsx @@ -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 { EuiPageContent } from '@elastic/eui'; +import { euiStyled } from '../../../../../../common/eui_styled_components'; + +export const LayoutContent = euiStyled(EuiPageContent)` + position: relative; +`; diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/node_details.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/metadata_details.tsx similarity index 72% rename from x-pack/legacy/plugins/infra/public/pages/metrics/components/node_details.tsx rename to x-pack/legacy/plugins/infra/public/pages/metrics/components/metadata_details.tsx index 5329ea992c493..c43f2d10d7163 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/node_details.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/metadata_details.tsx @@ -4,16 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback, useMemo } from 'react'; +import React, { useContext, useState, useCallback, useMemo } from 'react'; import { EuiButtonIcon, EuiFlexGrid, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; import { InfraMetadata } from '../../../../common/http_api'; import euiStyled from '../../../../../../common/eui_styled_components'; - -interface Props { - metadata?: InfraMetadata | null; -} +import { MetadataContext } from '../containers/metadata_context'; interface FieldDef { field: string; @@ -101,7 +98,13 @@ const getValueForField = (metadata: InfraMetadata, { field, isBoolean }: FieldDe return value; }; -export const NodeDetails = ({ metadata }: Props) => { +interface Props { + fields?: string[]; +} + +const NUMBER_OF_COLUMNS = 4; + +export const MetadataDetails = (props: Props) => { const [isOpen, setControlState] = useState(false); const toggleIsOpen = useCallback( @@ -109,38 +112,58 @@ export const NodeDetails = ({ metadata }: Props) => { [isOpen] ); - const fields = useMemo(() => (isOpen ? FIELDS : FIELDS.slice(0, 4)), [isOpen]); + const filteredFields = useMemo(() => { + if (props.fields && props.fields.length) { + return props.fields + .map(field => { + const fieldDef = FIELDS.find(f => f.field === field); + if (fieldDef) { + return fieldDef; + } + }) + .filter(f => f) as FieldDef[]; + } else { + return FIELDS; + } + }, [props.fields]); + const fields = useMemo( + () => (isOpen ? filteredFields : filteredFields.slice(0, NUMBER_OF_COLUMNS)), + [filteredFields, isOpen] + ); + const metadata = useContext(MetadataContext); if (!metadata) { return null; } return ( - - - - - + + {filteredFields.length > NUMBER_OF_COLUMNS ? ( + + + + ) : null} + {fields.map(field => ( -

{getLabelForField(field)}

+
{getLabelForField(field)}
{getValueForField(metadata, field)}
))}
-
+ ); }; -const NodeDetailsContainer = euiStyled.div` +const MetadataContainer = euiStyled.div` border-top: ${props => props.theme.eui.euiBorderWidthThin} solid ${props => props.theme.eui.euiBorderColor}; border-bottom: ${props => props.theme.eui.euiBorderWidthThin} solid ${props => @@ -153,4 +176,5 @@ display: flex; const Controls = euiStyled.div` flex-grow: 0; margin-right: ${props => props.theme.eui.paddingSizes.m}; +min-width: 0px; `; diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/node_details_page.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/node_details_page.tsx index 933831c6ec87d..2f4eb57cd5161 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/node_details_page.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/node_details_page.tsx @@ -12,7 +12,6 @@ import { EuiPageHeaderSection, EuiHideFor, EuiTitle, - EuiPageContent, } from '@elastic/eui'; import { InventoryMetric } from '../../../../common/inventory_models/types'; import { useNodeDetails } from '../../../containers/node_details/use_node_details'; @@ -20,13 +19,13 @@ import { InfraNodeType, InfraTimerangeInput } from '../../../graphql/types'; import { MetricsSideNav } from './side_nav'; import { AutoSizer } from '../../../components/auto_sizer'; import { MetricsTimeControls } from './time_controls'; -import { NodeDetails } from './node_details'; import { SideNavContext, NavItem } from '../lib/side_nav_context'; import { PageBody } from './page_body'; import euiStyled from '../../../../../../common/eui_styled_components'; import { MetricsTimeInput } from '../containers/with_metrics_time'; import { InfraMetadata } from '../../../../common/http_api/metadata_api'; import { PageError } from './page_error'; +import { MetadataContext } from '../../../pages/metrics/containers/metadata_context'; interface Props { name: string; @@ -100,14 +99,13 @@ export const NodeDetailsPage = (props: Props) => { - - - + + 0 && props.isAutoReloading ? false : loading} refetch={refetch} @@ -117,8 +115,8 @@ export const NodeDetailsPage = (props: Props) => { isLiveStreaming={props.isAutoReloading} stopLiveStreaming={() => props.setAutoReload(false)} /> - - + + ); @@ -128,10 +126,6 @@ export const NodeDetailsPage = (props: Props) => { ); }; -const EuiPageContentWithRelative = euiStyled(EuiPageContent)` - position: relative; -`; - const MetricsDetailsPageColumn = euiStyled.div` flex: 1 0 0%; display: flex; diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/containers/metadata_context.ts b/x-pack/legacy/plugins/infra/public/pages/metrics/containers/metadata_context.ts new file mode 100644 index 0000000000000..4ecf7fa15548c --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/containers/metadata_context.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { InfraMetadata } from '../../../../common/http_api'; +export const MetadataContext = React.createContext(null); From b34852a5bc3a9e013c9cd23cc3a345947b524fde Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Wed, 18 Dec 2019 10:21:10 -0700 Subject: [PATCH 09/61] Telemetry usage collection welcome screen (#53084) * Changes text dynamically based on the value of optIn in kibana.yml --- .../__snapshots__/home.test.js.snap | 2 +- .../__snapshots__/welcome.test.tsx.snap | 263 +++++++++++++++++- .../kibana/public/home/components/home.js | 11 +- .../public/home/components/home.test.js | 4 + .../kibana/public/home/components/home_app.js | 4 +- .../public/home/components/welcome.test.tsx | 24 +- .../kibana/public/home/components/welcome.tsx | 83 +++--- 7 files changed, 346 insertions(+), 45 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap index 0bf8c808ae920..c1131cbe559f6 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap @@ -1072,8 +1072,8 @@ exports[`home welcome should show the normal home page if welcome screen is disa exports[`home welcome should show the welcome screen if enabled, and there are no index patterns defined 1`] = ` `; diff --git a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/welcome.test.tsx.snap b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/welcome.test.tsx.snap index 2007a3bb773cf..e36a6e0a5a9fb 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/welcome.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/welcome.test.tsx.snap @@ -62,10 +62,46 @@ exports[`should render a Welcome screen with no telemetry disclaimer 1`] = ` + + + + + + + + + + @@ -138,6 +174,231 @@ exports[`should render a Welcome screen with the telemetry disclaimer 1`] = ` + + + + + + + + + + + + + + +
+
+ +`; + +exports[`should render a Welcome screen with the telemetry disclaimer when optIn is false 1`] = ` + +
+
+
+ + + + + +

+ +

+
+ +

+ +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + +
+
+
+`; + +exports[`should render a Welcome screen with the telemetry disclaimer when optIn is true 1`] = ` + +
+
+
+ + + + + +

+ +

+
+ +

+ +

+
+ +
+
+
+ + + diff --git a/src/legacy/core_plugins/kibana/public/home/components/home.js b/src/legacy/core_plugins/kibana/public/home/components/home.js index c87ceb9777c74..d552dd070c86d 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/home.js +++ b/src/legacy/core_plugins/kibana/public/home/components/home.js @@ -51,10 +51,7 @@ export class Home extends Component { getServices().getInjected('disableWelcomeScreen') || props.localStorage.getItem(KEY_ENABLE_WELCOME) === 'false' ); - const showTelemetryDisclaimer = getServices().getInjected( - 'telemetryNotifyUserAboutOptInDefault' - ); - + const currentOptInStatus = this.props.getOptInStatus(); this.state = { // If welcome is enabled, we wait for loading to complete // before rendering. This prevents an annoying flickering @@ -63,7 +60,7 @@ export class Home extends Component { isLoading: isWelcomeEnabled, isNewKibanaInstance: false, isWelcomeEnabled, - showTelemetryDisclaimer, + currentOptInStatus, }; } @@ -222,14 +219,13 @@ export class Home extends Component { renderLoading() { return ''; } - renderWelcome() { return ( ); } @@ -269,4 +265,5 @@ Home.propTypes = { urlBasePath: PropTypes.string.isRequired, mlEnabled: PropTypes.bool.isRequired, onOptInSeen: PropTypes.func.isRequired, + getOptInStatus: PropTypes.func.isRequired, }; diff --git a/src/legacy/core_plugins/kibana/public/home/components/home.test.js b/src/legacy/core_plugins/kibana/public/home/components/home.test.js index 780e2af695381..1f46cf2875fee 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/home.test.js +++ b/src/legacy/core_plugins/kibana/public/home/components/home.test.js @@ -63,6 +63,10 @@ describe('home', () => { setItem: sinon.mock(), }, urlBasePath: 'goober', + onOptInSeen() { + return false; + }, + getOptInStatus: jest.fn(), }; }); diff --git a/src/legacy/core_plugins/kibana/public/home/components/home_app.js b/src/legacy/core_plugins/kibana/public/home/components/home_app.js index 5a12eb0a66cf1..29f24f5b841a3 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/home_app.js +++ b/src/legacy/core_plugins/kibana/public/home/components/home_app.js @@ -29,14 +29,13 @@ import { getTutorial } from '../load_tutorials'; import { replaceTemplateStrings } from './tutorial/replace_template_strings'; import { getServices } from '../kibana_services'; import { npSetup } from 'ui/new_platform'; - export function HomeApp({ directories }) { const { getInjected, savedObjectsClient, getBasePath, addBasePath, - telemetryOptInProvider: { setOptInNoticeSeen }, + telemetryOptInProvider: { setOptInNoticeSeen, getOptIn }, } = getServices(); const { cloud } = npSetup.plugins; const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); @@ -87,6 +86,7 @@ export function HomeApp({ directories }) { localStorage={localStorage} urlBasePath={getBasePath()} onOptInSeen={setOptInNoticeSeen} + getOptInStatus={getOptIn} /> diff --git a/src/legacy/core_plugins/kibana/public/home/components/welcome.test.tsx b/src/legacy/core_plugins/kibana/public/home/components/welcome.test.tsx index 21dcfd9ef15de..42c6e6ff6056a 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/welcome.test.tsx +++ b/src/legacy/core_plugins/kibana/public/home/components/welcome.test.tsx @@ -35,7 +35,25 @@ jest.mock('../kibana_services', () => ({ test('should render a Welcome screen with the telemetry disclaimer', () => { const component = shallow( // @ts-ignore - {}} showTelemetryDisclaimer={true} onOptInSeen={() => {}} /> + {}} onOptInSeen={() => {}} /> + ); + + expect(component).toMatchSnapshot(); +}); + +test('should render a Welcome screen with the telemetry disclaimer when optIn is true', () => { + const component = shallow( + // @ts-ignore + {}} onOptInSeen={() => {}} currentOptInStatus={true} /> + ); + + expect(component).toMatchSnapshot(); +}); + +test('should render a Welcome screen with the telemetry disclaimer when optIn is false', () => { + const component = shallow( + // @ts-ignore + {}} onOptInSeen={() => {}} currentOptInStatus={false} /> ); expect(component).toMatchSnapshot(); @@ -45,7 +63,7 @@ test('should render a Welcome screen with no telemetry disclaimer', () => { // @ts-ignore const component = shallow( // @ts-ignore - {}} showTelemetryDisclaimer={false} onOptInSeen={() => {}} /> + {}} onOptInSeen={() => {}} /> ); expect(component).toMatchSnapshot(); @@ -56,7 +74,7 @@ test('fires opt-in seen when mounted', () => { shallow( // @ts-ignore - {}} showTelemetryDisclaimer={true} onOptInSeen={seen} /> + {}} onOptInSeen={seen} /> ); expect(seen).toHaveBeenCalled(); diff --git a/src/legacy/core_plugins/kibana/public/home/components/welcome.tsx b/src/legacy/core_plugins/kibana/public/home/components/welcome.tsx index c8de0bf7bb936..435bf98ca7840 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/welcome.tsx +++ b/src/legacy/core_plugins/kibana/public/home/components/welcome.tsx @@ -23,7 +23,7 @@ * in Elasticsearch. */ -import React from 'react'; +import React, { Fragment } from 'react'; import { EuiLink, EuiTextColor, @@ -39,12 +39,11 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { getServices } from '../kibana_services'; import { SampleDataCard } from './sample_data'; - interface Props { urlBasePath: string; onSkip: () => void; onOptInSeen: () => any; - showTelemetryDisclaimer: boolean; + currentOptInStatus: boolean; } /** @@ -84,9 +83,42 @@ export class Welcome extends React.Component { document.removeEventListener('keydown', this.hideOnEsc); } - render() { - const { urlBasePath, showTelemetryDisclaimer } = this.props; + private renderTelemetryEnabledOrDisabledText = () => { + if (this.props.currentOptInStatus) { + return ( + + + + + + + ); + } else { + return ( + + + + + + + ); + } + }; + render() { + const { urlBasePath } = this.props; return (
@@ -121,34 +153,23 @@ export class Welcome extends React.Component { onDecline={this.onSampleDataDecline} /> - {showTelemetryDisclaimer && ( - - - - - + + + - - - - - )} + + {this.renderTelemetryEnabledOrDisabledText()} + From 44348aa9983659219c6f143c09e22d4cfcbdafb4 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 18 Dec 2019 17:31:31 +0000 Subject: [PATCH 10/61] Aligns Alerting's interval with TaskManager's generic schedule field (#52873) Follow up from the #52727 in Task Manager, we want Alerting and Task Manager to align on their schedule api (in the near future, Alerting will actually use Task manager's schedule system to remove this duplication). --- x-pack/legacy/plugins/alerting/README.md | 12 +- x-pack/legacy/plugins/alerting/mappings.json | 8 +- .../alerting/server/alerts_client.test.ts | 114 +++++++++++------- .../plugins/alerting/server/alerts_client.ts | 25 ++-- .../server/lib/get_next_run_at.test.ts | 4 +- .../alerting/server/lib/get_next_run_at.ts | 5 +- .../server/lib/task_runner_factory.test.ts | 2 +- .../server/lib/task_runner_factory.ts | 11 +- .../alerting/server/routes/create.test.ts | 10 +- .../plugins/alerting/server/routes/create.ts | 9 +- .../alerting/server/routes/get.test.ts | 2 +- .../alerting/server/routes/update.test.ts | 8 +- .../plugins/alerting/server/routes/update.ts | 9 +- .../legacy/plugins/alerting/server/types.ts | 8 +- .../routes/__mocks__/request_responses.ts | 2 +- .../detection_engine/routes/rules/utils.ts | 2 +- .../detection_engine/rules/create_rules.ts | 2 +- .../detection_engine/rules/update_rules.ts | 13 +- .../common/lib/alert_utils.ts | 2 +- .../common/lib/get_test_alert_data.ts | 2 +- .../tests/alerting/alerts.ts | 12 +- .../tests/alerting/create.ts | 32 +++-- .../tests/alerting/find.ts | 4 +- .../security_and_spaces/tests/alerting/get.ts | 2 +- .../tests/alerting/update.ts | 26 ++-- .../spaces_only/tests/alerting/alerts.ts | 2 +- .../spaces_only/tests/alerting/create.ts | 2 +- .../spaces_only/tests/alerting/find.ts | 2 +- .../spaces_only/tests/alerting/get.ts | 2 +- .../spaces_only/tests/alerting/update.ts | 4 +- 30 files changed, 212 insertions(+), 126 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/README.md b/x-pack/legacy/plugins/alerting/README.md index 85dbd75e14174..0b4024be39548 100644 --- a/x-pack/legacy/plugins/alerting/README.md +++ b/x-pack/legacy/plugins/alerting/README.md @@ -200,7 +200,7 @@ Payload: |name|A name to reference and search in the future.|string| |tags|A list of keywords to reference and search in the future.|string[]| |alertTypeId|The id value of the alert type you want to call when the alert is scheduled to execute.|string| -|interval|The interval in seconds, minutes, hours or days the alert should execute. Example: `10s`, `5m`, `1h`, `1d`.|string| +|schedule|The schedule specifying when this alert should run, using one of the available schedule formats specified under _Schedule Formats_ below|object| |params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| |actions|Array of the following:
- `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
- `id` (string): The id of the action saved object to execute.
- `params` (object): The map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| @@ -242,7 +242,7 @@ Payload: |Property|Description|Type| |---|---|---| -|interval|The interval in seconds, minutes, hours or days the alert should execute. Example: `10s`, `5m`, `1h`, `1d`.|string| +|schedule|The schedule specifying when this alert should be run, using one of the available schedule formats specified under _Schedule Formats_ below|object| |name|A name to reference and search in the future.|string| |tags|A list of keywords to reference and search in the future.|string[]| |params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| @@ -304,6 +304,14 @@ Params: |---|---|---| |id|The id of the alert you're trying to update the API key for. System will use user in request context to generate an API key for.|string| +##### Schedule Formats +A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. + +We currently support the _Interval format_ which specifies the interval in seconds, minutes, hours or days at which the alert should execute. +Example: `{ interval: "10s" }`, `{ interval: "5m" }`, `{ interval: "1h" }`, `{ interval: "1d" }`. + +There are plans to support multiple other schedule formats in the near fuiture. + ## Alert instance factory **alertInstanceFactory(id)** diff --git a/x-pack/legacy/plugins/alerting/mappings.json b/x-pack/legacy/plugins/alerting/mappings.json index 7a7446602351d..9536187116031 100644 --- a/x-pack/legacy/plugins/alerting/mappings.json +++ b/x-pack/legacy/plugins/alerting/mappings.json @@ -13,8 +13,12 @@ "alertTypeId": { "type": "keyword" }, - "interval": { - "type": "keyword" + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } }, "actions": { "type": "nested", diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 37eb6a9b21d44..b07dad68da72d 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -47,7 +47,7 @@ function getMockData(overwrites: Record = {}) { name: 'abc', tags: ['foo'], alertTypeId: '123', - interval: '10s', + schedule: { interval: '10s' }, throttle: null, params: { bar: true, @@ -92,7 +92,7 @@ describe('create()', () => { type: 'alert', attributes: { alertTypeId: '123', - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, @@ -157,10 +157,12 @@ describe('create()', () => { ], "alertTypeId": "123", "id": "1", - "interval": "10s", "params": Object { "bar": true, }, + "schedule": Object { + "interval": "10s", + }, "scheduledTaskId": "task-123", } `); @@ -184,13 +186,15 @@ describe('create()', () => { "apiKeyOwner": undefined, "createdBy": "elastic", "enabled": true, - "interval": "10s", "muteAll": false, "mutedInstanceIds": Array [], "name": "abc", "params": Object { "bar": true, }, + "schedule": Object { + "interval": "10s", + }, "tags": Array [ "foo", ], @@ -298,7 +302,7 @@ describe('create()', () => { type: 'alert', attributes: { alertTypeId: '123', - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, @@ -399,10 +403,12 @@ describe('create()', () => { ], "alertTypeId": "123", "id": "1", - "interval": "10s", "params": Object { "bar": true, }, + "schedule": Object { + "interval": "10s", + }, "scheduledTaskId": "task-123", } `); @@ -445,7 +451,7 @@ describe('create()', () => { attributes: { enabled: false, alertTypeId: '123', - interval: 10000, + schedule: { interval: 10000 }, params: { bar: true, }, @@ -484,10 +490,12 @@ describe('create()', () => { "alertTypeId": "123", "enabled": false, "id": "1", - "interval": 10000, "params": Object { "bar": true, }, + "schedule": Object { + "interval": 10000, + }, } `); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); @@ -585,7 +593,7 @@ describe('create()', () => { type: 'alert', attributes: { alertTypeId: '123', - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, @@ -648,7 +656,7 @@ describe('create()', () => { type: 'alert', attributes: { alertTypeId: '123', - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, @@ -722,7 +730,7 @@ describe('create()', () => { type: 'alert', attributes: { alertTypeId: '123', - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, @@ -794,7 +802,7 @@ describe('create()', () => { createdBy: 'elastic', updatedBy: 'elastic', enabled: true, - interval: '10s', + schedule: { interval: '10s' }, throttle: null, muteAll: false, mutedInstanceIds: [], @@ -820,7 +828,7 @@ describe('enable()', () => { id: '1', type: 'alert', attributes: { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: false, }, @@ -846,7 +854,7 @@ describe('enable()', () => { 'alert', '1', { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, scheduledTaskId: 'task-123', @@ -879,7 +887,7 @@ describe('enable()', () => { id: '1', type: 'alert', attributes: { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, }, @@ -897,7 +905,7 @@ describe('enable()', () => { id: '1', type: 'alert', attributes: { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: false, }, @@ -927,7 +935,7 @@ describe('enable()', () => { 'alert', '1', { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, scheduledTaskId: 'task-123', @@ -962,7 +970,7 @@ describe('disable()', () => { id: '1', type: 'alert', attributes: { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, scheduledTaskId: 'task-123', @@ -976,7 +984,7 @@ describe('disable()', () => { 'alert', '1', { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', apiKey: null, apiKeyOwner: null, @@ -997,7 +1005,7 @@ describe('disable()', () => { id: '1', type: 'alert', attributes: { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: false, scheduledTaskId: 'task-123', @@ -1060,7 +1068,7 @@ describe('muteInstance()', () => { id: '1', type: 'alert', attributes: { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, scheduledTaskId: 'task-123', @@ -1088,7 +1096,7 @@ describe('muteInstance()', () => { id: '1', type: 'alert', attributes: { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, scheduledTaskId: 'task-123', @@ -1107,7 +1115,7 @@ describe('muteInstance()', () => { id: '1', type: 'alert', attributes: { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, scheduledTaskId: 'task-123', @@ -1129,7 +1137,7 @@ describe('unmuteInstance()', () => { id: '1', type: 'alert', attributes: { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, scheduledTaskId: 'task-123', @@ -1157,7 +1165,7 @@ describe('unmuteInstance()', () => { id: '1', type: 'alert', attributes: { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, scheduledTaskId: 'task-123', @@ -1176,7 +1184,7 @@ describe('unmuteInstance()', () => { id: '1', type: 'alert', attributes: { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, scheduledTaskId: 'task-123', @@ -1199,7 +1207,7 @@ describe('get()', () => { type: 'alert', attributes: { alertTypeId: '123', - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, @@ -1235,10 +1243,12 @@ describe('get()', () => { ], "alertTypeId": "123", "id": "1", - "interval": "10s", "params": Object { "bar": true, }, + "schedule": Object { + "interval": "10s", + }, } `); expect(savedObjectsClient.get).toHaveBeenCalledTimes(1); @@ -1257,7 +1267,7 @@ describe('get()', () => { type: 'alert', attributes: { alertTypeId: '123', - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, @@ -1292,7 +1302,7 @@ describe('find()', () => { type: 'alert', attributes: { alertTypeId: '123', - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, @@ -1332,10 +1342,12 @@ describe('find()', () => { ], "alertTypeId": "123", "id": "1", - "interval": "10s", "params": Object { "bar": true, }, + "schedule": Object { + "interval": "10s", + }, }, ], "page": 1, @@ -1362,7 +1374,7 @@ describe('delete()', () => { type: 'alert', attributes: { alertTypeId: '123', - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, @@ -1443,7 +1455,7 @@ describe('update()', () => { type: 'alert', attributes: { enabled: true, - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, @@ -1470,7 +1482,7 @@ describe('update()', () => { const result = await alertsClient.update({ id: '1', data: { - interval: '10s', + schedule: { interval: '10s' }, name: 'abc', tags: ['foo'], params: { @@ -1501,10 +1513,12 @@ describe('update()', () => { ], "enabled": true, "id": "1", - "interval": "10s", "params": Object { "bar": true, }, + "schedule": Object { + "interval": "10s", + }, "scheduledTaskId": "task-123", } `); @@ -1528,11 +1542,13 @@ describe('update()', () => { "apiKey": null, "apiKeyOwner": null, "enabled": true, - "interval": "10s", "name": "abc", "params": Object { "bar": true, }, + "schedule": Object { + "interval": "10s", + }, "scheduledTaskId": "task-123", "tags": Array [ "foo", @@ -1598,7 +1614,7 @@ describe('update()', () => { type: 'alert', attributes: { enabled: true, - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, @@ -1651,7 +1667,7 @@ describe('update()', () => { const result = await alertsClient.update({ id: '1', data: { - interval: '10s', + schedule: { interval: '10s' }, name: 'abc', tags: ['foo'], params: { @@ -1712,10 +1728,12 @@ describe('update()', () => { ], "enabled": true, "id": "1", - "interval": "10s", "params": Object { "bar": true, }, + "schedule": Object { + "interval": "10s", + }, "scheduledTaskId": "task-123", } `); @@ -1771,7 +1789,7 @@ describe('update()', () => { type: 'alert', attributes: { enabled: true, - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, @@ -1799,7 +1817,7 @@ describe('update()', () => { const result = await alertsClient.update({ id: '1', data: { - interval: '10s', + schedule: { interval: '10s' }, name: 'abc', tags: ['foo'], params: { @@ -1831,10 +1849,12 @@ describe('update()', () => { "apiKey": "MTIzOmFiYw==", "enabled": true, "id": "1", - "interval": "10s", "params": Object { "bar": true, }, + "schedule": Object { + "interval": "10s", + }, "scheduledTaskId": "task-123", } `); @@ -1858,11 +1878,13 @@ describe('update()', () => { "apiKey": "MTIzOmFiYw==", "apiKeyOwner": "elastic", "enabled": true, - "interval": "10s", "name": "abc", "params": Object { "bar": true, }, + "schedule": Object { + "interval": "10s", + }, "scheduledTaskId": "task-123", "tags": Array [ "foo", @@ -1909,7 +1931,7 @@ describe('update()', () => { alertsClient.update({ id: '1', data: { - interval: '10s', + schedule: { interval: '10s' }, name: 'abc', tags: ['foo'], params: { @@ -1939,7 +1961,7 @@ describe('updateApiKey()', () => { id: '1', type: 'alert', attributes: { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, }, @@ -1956,7 +1978,7 @@ describe('updateApiKey()', () => { 'alert', '1', { - interval: '10s', + schedule: { interval: '10s' }, alertTypeId: '2', enabled: true, apiKey: Buffer.from('123:abc').toString('base64'), diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index 27fda9871e685..578daa445b6ff 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -8,7 +8,14 @@ import Boom from 'boom'; import { omit } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Logger, SavedObjectsClientContract, SavedObjectReference } from 'src/core/server'; -import { Alert, RawAlert, AlertTypeRegistry, AlertAction, AlertType } from './types'; +import { + Alert, + RawAlert, + AlertTypeRegistry, + AlertAction, + AlertType, + IntervalSchedule, +} from './types'; import { TaskManagerStartContract } from './shim'; import { validateAlertTypeParams } from './lib'; import { CreateAPIKeyResult as SecurityPluginCreateAPIKeyResult } from '../../../../plugins/security/server'; @@ -82,7 +89,7 @@ interface UpdateOptions { data: { name: string; tags: string[]; - interval: string; + schedule: IntervalSchedule; actions: NormalizedAlertAction[]; params: Record; }; @@ -145,11 +152,7 @@ export class AlertsClient { if (data.enabled) { let scheduledTask; try { - scheduledTask = await this.scheduleAlert( - createdAlert.id, - rawAlert.alertTypeId, - rawAlert.interval - ); + scheduledTask = await this.scheduleAlert(createdAlert.id, rawAlert.alertTypeId); } catch (e) { // Cleanup data, something went wrong scheduling the task try { @@ -259,11 +262,7 @@ export class AlertsClient { const { attributes, version } = await this.savedObjectsClient.get('alert', id); if (attributes.enabled === false) { const apiKey = await this.createAPIKey(); - const scheduledTask = await this.scheduleAlert( - id, - attributes.alertTypeId, - attributes.interval - ); + const scheduledTask = await this.scheduleAlert(id, attributes.alertTypeId); const username = await this.getUserName(); await this.savedObjectsClient.update( 'alert', @@ -364,7 +363,7 @@ export class AlertsClient { } } - private async scheduleAlert(id: string, alertTypeId: string, interval: string) { + private async scheduleAlert(id: string, alertTypeId: string) { return await this.taskManager.schedule({ taskType: `alerting:${alertTypeId}`, params: { diff --git a/x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.test.ts b/x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.test.ts index 852e412689b35..1c4d8a42d2830 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.test.ts @@ -15,12 +15,12 @@ const mockedNow = new Date('2019-06-03T18:55:25.982Z'); test('Adds interface to given date when result is > Date.now()', () => { const currentRunAt = new Date('2019-06-03T18:55:23.982Z'); - const result = getNextRunAt(currentRunAt, '10s'); + const result = getNextRunAt(currentRunAt, { interval: '10s' }); expect(result).toEqual(new Date('2019-06-03T18:55:33.982Z')); }); test('Uses Date.now() when the result would of been a date in the past', () => { const currentRunAt = new Date('2019-06-03T18:55:13.982Z'); - const result = getNextRunAt(currentRunAt, '10s'); + const result = getNextRunAt(currentRunAt, { interval: '10s' }); expect(result).toEqual(mockedNow); }); diff --git a/x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.ts b/x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.ts index 901b614b4d68c..f9867b5372908 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/get_next_run_at.ts @@ -5,9 +5,10 @@ */ import { parseDuration } from './parse_duration'; +import { IntervalSchedule } from '../types'; -export function getNextRunAt(currentRunAt: Date, interval: string) { - let nextRunAt = currentRunAt.getTime() + parseDuration(interval); +export function getNextRunAt(currentRunAt: Date, schedule: IntervalSchedule) { + let nextRunAt = currentRunAt.getTime() + parseDuration(schedule.interval); if (nextRunAt < Date.now()) { // To prevent returning dates in the past, we'll return now instead nextRunAt = Date.now(); diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts index c21c419977bbe..7966f98c749c8 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.test.ts @@ -74,7 +74,7 @@ const mockedAlertTypeSavedObject = { attributes: { enabled: true, alertTypeId: '123', - interval: '10s', + schedule: { interval: '10s' }, mutedInstanceIds: [], params: { bar: true, diff --git a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts index 051b15fc8dd8f..fe0979538d04e 100644 --- a/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/lib/task_runner_factory.ts @@ -20,6 +20,7 @@ import { GetServicesFunction, RawAlert, SpaceIdToNamespaceFunction, + IntervalSchedule, } from '../types'; export interface TaskRunnerContext { @@ -94,7 +95,7 @@ export class TaskRunnerFactory { const services = getServices(fakeRequest); // Ensure API key is still valid and user has access const { - attributes: { params, actions, interval, throttle, muteAll, mutedInstanceIds }, + attributes: { params, actions, schedule, throttle, muteAll, mutedInstanceIds }, references, } = await services.savedObjectsClient.get('alert', alertId); @@ -167,7 +168,13 @@ export class TaskRunnerFactory { }) ); - const nextRunAt = getNextRunAt(new Date(taskInstance.startedAt!), interval); + const nextRunAt = getNextRunAt( + new Date(taskInstance.startedAt!), + // we do not currently have a good way of returning the type + // from SavedObjectsClient, and as we currenrtly require a schedule + // and we only support `interval`, we can cast this safely + schedule as IntervalSchedule + ); return { state: { diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts index 634a797880812..a804aff55ad42 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.test.ts @@ -13,7 +13,7 @@ server.route(createAlertRoute); const mockedAlert = { alertTypeId: '1', name: 'abc', - interval: '10s', + schedule: { interval: '10s' }, tags: ['foo'], params: { bar: true, @@ -65,11 +65,13 @@ test('creates an alert with proper parameters', async () => { ], "alertTypeId": "1", "id": "123", - "interval": "10s", "name": "abc", "params": Object { "bar": true, }, + "schedule": Object { + "interval": "10s", + }, "tags": Array [ "foo", ], @@ -91,11 +93,13 @@ test('creates an alert with proper parameters', async () => { ], "alertTypeId": "1", "enabled": true, - "interval": "10s", "name": "abc", "params": Object { "bar": true, }, + "schedule": Object { + "interval": "10s", + }, "tags": Array [ "foo", ], diff --git a/x-pack/legacy/plugins/alerting/server/routes/create.ts b/x-pack/legacy/plugins/alerting/server/routes/create.ts index cb5277ae19100..417072f978a92 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/create.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/create.ts @@ -7,6 +7,7 @@ import Hapi from 'hapi'; import Joi from 'joi'; import { getDurationSchema } from '../lib'; +import { IntervalSchedule } from '../types'; interface ScheduleRequest extends Hapi.Request { payload: { @@ -14,7 +15,7 @@ interface ScheduleRequest extends Hapi.Request { name: string; tags: string[]; alertTypeId: string; - interval: string; + schedule: IntervalSchedule; actions: Array<{ group: string; id: string; @@ -43,7 +44,11 @@ export const createAlertRoute = { .default([]), alertTypeId: Joi.string().required(), throttle: getDurationSchema().default(null), - interval: getDurationSchema().required(), + schedule: Joi.object() + .keys({ + interval: getDurationSchema().required(), + }) + .required(), params: Joi.object().required(), actions: Joi.array() .items( diff --git a/x-pack/legacy/plugins/alerting/server/routes/get.test.ts b/x-pack/legacy/plugins/alerting/server/routes/get.test.ts index 4d44ee9dfe6bd..b97762d10c960 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/get.test.ts @@ -13,7 +13,7 @@ server.route(getAlertRoute); const mockedAlert = { id: '1', alertTypeId: '1', - interval: '10s', + schedule: { interval: '10s' }, params: { bar: true, }, diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts index 334fb2120319d..8ce9d94140e6d 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.test.ts @@ -16,7 +16,7 @@ const mockedResponse = { id: '1', alertTypeId: '1', tags: ['foo'], - interval: '12s', + schedule: { interval: '12s' }, params: { otherField: false, }, @@ -40,7 +40,7 @@ test('calls the update function with proper parameters', async () => { throttle: null, name: 'abc', tags: ['bar'], - interval: '12s', + schedule: { interval: '12s' }, params: { otherField: false, }, @@ -75,11 +75,13 @@ test('calls the update function with proper parameters', async () => { }, }, ], - "interval": "12s", "name": "abc", "params": Object { "otherField": false, }, + "schedule": Object { + "interval": "12s", + }, "tags": Array [ "bar", ], diff --git a/x-pack/legacy/plugins/alerting/server/routes/update.ts b/x-pack/legacy/plugins/alerting/server/routes/update.ts index 6e8f8557fb24a..bc55d48465602 100644 --- a/x-pack/legacy/plugins/alerting/server/routes/update.ts +++ b/x-pack/legacy/plugins/alerting/server/routes/update.ts @@ -7,6 +7,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { getDurationSchema } from '../lib'; +import { IntervalSchedule } from '../types'; interface UpdateRequest extends Hapi.Request { params: { @@ -16,7 +17,7 @@ interface UpdateRequest extends Hapi.Request { alertTypeId: string; name: string; tags: string[]; - interval: string; + schedule: IntervalSchedule; actions: Array<{ group: string; id: string; @@ -45,7 +46,11 @@ export const updateAlertRoute = { tags: Joi.array() .items(Joi.string()) .required(), - interval: getDurationSchema().required(), + schedule: Joi.object() + .keys({ + interval: getDurationSchema().required(), + }) + .required(), params: Joi.object().required(), actions: Joi.array() .items( diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts index 1bec2632d8082..e06e0c45e20b4 100644 --- a/x-pack/legacy/plugins/alerting/server/types.ts +++ b/x-pack/legacy/plugins/alerting/server/types.ts @@ -60,12 +60,16 @@ export interface RawAlertAction extends SavedObjectAttributes { params: AlertActionParams; } +export interface IntervalSchedule extends SavedObjectAttributes { + interval: string; +} + export interface Alert { enabled: boolean; name: string; tags: string[]; alertTypeId: string; - interval: string; + schedule: IntervalSchedule; actions: AlertAction[]; params: Record; scheduledTaskId?: string; @@ -83,7 +87,7 @@ export interface RawAlert extends SavedObjectAttributes { name: string; tags: string[]; alertTypeId: string; - interval: string; + schedule: SavedObjectAttributes; actions: RawAlertAction[]; params: SavedObjectAttributes; scheduledTaskId?: string; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index ae205a814daae..3c5182b5178b3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -271,7 +271,7 @@ export const getResult = (): RuleAlertType => ({ references: ['http://www.example.com', 'https://ww.example.com'], version: 1, }, - interval: '5m', + schedule: { interval: '5m' }, enabled: true, actions: [], throttle: null, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index 88261d872b0ea..dad22c74398d2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -45,7 +45,7 @@ export const transformAlertToRule = (alert: RuleAlertType): Partial { + it('should handle create alert request appropriately when interval schedule is wrong syntax', async () => { const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) - .send(getTestAlertData(getTestAlertData({ interval: '10x' }))); + .send(getTestAlertData(getTestAlertData({ schedule: { interval: '10x' } }))); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -275,10 +275,15 @@ export default function createAlertTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'child "interval" fails because ["interval" with value "10x" fails to match the seconds pattern, "interval" with value "10x" fails to match the minutes pattern, "interval" with value "10x" fails to match the hours pattern, "interval" with value "10x" fails to match the days pattern]', + 'child "schedule" fails because [child "interval" fails because ["interval" with value "10x" fails to match the seconds pattern, "interval" with value "10x" fails to match the minutes pattern, "interval" with value "10x" fails to match the hours pattern, "interval" with value "10x" fails to match the days pattern]]', validation: { source: 'payload', - keys: ['interval', 'interval', 'interval', 'interval'], + keys: [ + 'schedule.interval', + 'schedule.interval', + 'schedule.interval', + 'schedule.interval', + ], }, }); break; @@ -287,12 +292,12 @@ export default function createAlertTests({ getService }: FtrProviderContext) { } }); - it('should handle create alert request appropriately when interval is 0', async () => { + it('should handle create alert request appropriately when interval schedule is 0', async () => { const response = await supertestWithoutAuth .post(`${getUrlPrefix(space.id)}/api/alert`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) - .send(getTestAlertData(getTestAlertData({ interval: '0s' }))); + .send(getTestAlertData(getTestAlertData({ schedule: { interval: '0s' } }))); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -312,10 +317,15 @@ export default function createAlertTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'child "interval" fails because ["interval" with value "0s" fails to match the seconds pattern, "interval" with value "0s" fails to match the minutes pattern, "interval" with value "0s" fails to match the hours pattern, "interval" with value "0s" fails to match the days pattern]', + 'child "schedule" fails because [child "interval" fails because ["interval" with value "0s" fails to match the seconds pattern, "interval" with value "0s" fails to match the minutes pattern, "interval" with value "0s" fails to match the hours pattern, "interval" with value "0s" fails to match the days pattern]]', validation: { source: 'payload', - keys: ['interval', 'interval', 'interval', 'interval'], + keys: [ + 'schedule.interval', + 'schedule.interval', + 'schedule.interval', + 'schedule.interval', + ], }, }); break; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts index 359058f2ac23a..4da6c059c5a5e 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/find.ts @@ -59,7 +59,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { name: 'abc', tags: ['foo'], alertTypeId: 'test.noop', - interval: '1m', + schedule: { interval: '1m' }, enabled: true, actions: [], params: {}, @@ -138,7 +138,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { name: 'abc', tags: ['foo'], alertTypeId: 'test.noop', - interval: '1m', + schedule: { interval: '1m' }, enabled: false, actions: [ { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts index 1a8109f6b6b3c..9c1f7fea93292 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/get.ts @@ -53,7 +53,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { name: 'abc', tags: ['foo'], alertTypeId: 'test.noop', - interval: '1m', + schedule: { interval: '1m' }, enabled: true, actions: [], params: {}, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index 1b1bcef9ad23f..0e2ec0f7bc534 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -36,7 +36,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { params: { foo: true, }, - interval: '12s', + schedule: { interval: '12s' }, actions: [], throttle: '2m', }; @@ -96,7 +96,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { params: { foo: true, }, - interval: '12s', + schedule: { interval: '12s' }, throttle: '1m', actions: [], }); @@ -145,7 +145,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { params: { foo: true, }, - interval: '12s', + schedule: { interval: '12s' }, actions: [], }); @@ -203,10 +203,10 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'child "throttle" fails because ["throttle" is required]. child "name" fails because ["name" is required]. child "tags" fails because ["tags" is required]. child "interval" fails because ["interval" is required]. child "params" fails because ["params" is required]. child "actions" fails because ["actions" is required]', + 'child "throttle" fails because ["throttle" is required]. child "name" fails because ["name" is required]. child "tags" fails because ["tags" is required]. child "schedule" fails because ["schedule" is required]. child "params" fails because ["params" is required]. child "actions" fails because ["actions" is required]', validation: { source: 'payload', - keys: ['throttle', 'name', 'tags', 'interval', 'params', 'actions'], + keys: ['throttle', 'name', 'tags', 'schedule', 'params', 'actions'], }, }); break; @@ -237,7 +237,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .send({ name: 'bcd', tags: ['bar'], - interval: '1m', + schedule: { interval: '1m' }, throttle: '1m', params: {}, actions: [], @@ -269,12 +269,12 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { } }); - it('should handle update alert request appropriately when interval is wrong syntax', async () => { + it('should handle update alert request appropriately when interval schedule is wrong syntax', async () => { const response = await supertestWithoutAuth .put(`${getUrlPrefix(space.id)}/api/alert/1`) .set('kbn-xsrf', 'foo') .auth(user.username, user.password) - .send(getTestAlertData({ interval: '10x', enabled: undefined })); + .send(getTestAlertData({ schedule: { interval: '10x' }, enabled: undefined })); switch (scenario.id) { case 'no_kibana_privileges at space1': @@ -294,10 +294,16 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'child "interval" fails because ["interval" with value "10x" fails to match the seconds pattern, "interval" with value "10x" fails to match the minutes pattern, "interval" with value "10x" fails to match the hours pattern, "interval" with value "10x" fails to match the days pattern]. "alertTypeId" is not allowed', + 'child "schedule" fails because [child "interval" fails because ["interval" with value "10x" fails to match the seconds pattern, "interval" with value "10x" fails to match the minutes pattern, "interval" with value "10x" fails to match the hours pattern, "interval" with value "10x" fails to match the days pattern]]. "alertTypeId" is not allowed', validation: { source: 'payload', - keys: ['interval', 'interval', 'interval', 'interval', 'alertTypeId'], + keys: [ + 'schedule.interval', + 'schedule.interval', + 'schedule.interval', + 'schedule.interval', + 'alertTypeId', + ], }, }); break; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts index 5fafd8b0bfb61..03e973194b4e2 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts.ts @@ -123,7 +123,7 @@ export default function alertTests({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'foo') .send( getTestAlertData({ - interval: '1m', + schedule: { interval: '1m' }, alertTypeId: 'test.always-firing', params: { index: ES_TEST_INDEX_NAME, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 929905a958abb..0e9011729eb3e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -71,7 +71,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { alertTypeId: 'test.noop', params: {}, createdBy: null, - interval: '1m', + schedule: { interval: '1m' }, scheduledTaskId: response.body.scheduledTaskId, updatedBy: null, throttle: '1m', diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts index 0d12af6db79b2..3fdd9168eb5cb 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/find.ts @@ -42,7 +42,7 @@ export default function createFindTests({ getService }: FtrProviderContext) { name: 'abc', tags: ['foo'], alertTypeId: 'test.noop', - interval: '1m', + schedule: { interval: '1m' }, enabled: true, actions: [], params: {}, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts index 9e4797bcbf7ad..a49d3478d336d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/get.ts @@ -36,7 +36,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { name: 'abc', tags: ['foo'], alertTypeId: 'test.noop', - interval: '1m', + schedule: { interval: '1m' }, enabled: true, actions: [], params: {}, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts index a6eccf88d9e26..46822781c0cd3 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/update.ts @@ -31,7 +31,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { params: { foo: true, }, - interval: '12s', + schedule: { interval: '12s' }, actions: [], throttle: '1m', }; @@ -71,7 +71,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { params: { foo: true, }, - interval: '12s', + schedule: { interval: '12s' }, actions: [], throttle: '1m', }) From 942fdeeb2d9a32622d8d3929e484dde5c6e16074 Mon Sep 17 00:00:00 2001 From: Alex Holmansky Date: Wed, 18 Dec 2019 12:40:58 -0500 Subject: [PATCH 11/61] Fix a typo in PR project assigner workflow configuration (#53511) * Workflow configurations to assign issues and PRs with Team:AppArch label to kibana-app-arch project * Fix a typo in the label name * Remove merge conflicts --- .github/workflows/pr-project-assigner.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml index aea8a9cad6b1f..8eab1b99957cd 100644 --- a/.github/workflows/pr-project-assigner.yml +++ b/.github/workflows/pr-project-assigner.yml @@ -11,5 +11,5 @@ jobs: uses: elastic/github-actions/project-assigner@v1.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Team:AppAch", "projectName": "kibana-app-arch", "columnId": 6173897}]' - ghToken: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + issue-mappings: '[{"label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173897}]' + ghToken: ${{ secrets.GITHUB_TOKEN }} From 068f3ffa2c0e6e45ddbaa1228f057887cc024f9a Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 18 Dec 2019 11:15:14 -0700 Subject: [PATCH 12/61] Upgrade to EUI@17.1.2 [table typescript types] (#52688) * more types * table types changes * siem table conversions * Updated rest of x-pack for EUI table typees * updated x-pack changes against master * Update to published eui 17.1.0 * kibana snapshots * x-pack snapshots * src snapshots * autofixes * autofixes, round 2 --- package.json | 2 +- .../flyout_service.test.tsx.snap | 4 +- .../__snapshots__/modal_service.test.tsx.snap | 2 +- .../dashboard_empty_screen.test.tsx.snap | 14 +- .../__snapshots__/no_results.test.js.snap | 4 + .../saved_objects_installer.test.js.snap | 5 +- .../__jest__/__snapshots__/table.test.js.snap | 3 +- .../__jest__/__snapshots__/table.test.js.snap | 1 - .../__jest__/__snapshots__/table.test.js.snap | 1 - .../index_pattern_table.tsx | 4 +- .../__snapshots__/objects_table.test.js.snap | 1 - .../__snapshots__/flyout.test.js.snap | 4 - .../__snapshots__/relationships.test.js.snap | 8 - .../__snapshots__/new_vis_modal.test.tsx.snap | 108 +++++++++++--- .../visualization_noresults.test.js.snap | 1 + .../visualization_requesterror.test.js.snap | 1 + .../shard_failure_table.test.tsx.snap | 1 - .../fetch/components/shard_failure_table.tsx | 14 +- .../__snapshots__/field_name.test.tsx.snap | 6 + .../exit_full_screen_button.test.js.snap | 2 + .../vislib_vis_legend.test.tsx.snap | 4 +- .../inspector_panel.test.tsx.snap | 6 +- .../inspector/public/views/data/types.ts | 4 +- .../exit_full_screen_button.test.tsx.snap | 4 + .../table_list_view/table_list_view.tsx | 6 +- .../plugins/kbn_tp_run_pipeline/package.json | 2 +- .../kbn_tp_custom_visualizations/package.json | 2 +- .../kbn_tp_embeddable_explorer/package.json | 2 +- .../kbn_tp_sample_panel_action/package.json | 2 +- typings/@elastic/eui/index.d.ts | 13 -- .../__test__/__snapshots__/List.test.tsx.snap | 96 ++++++++---- .../ServiceOverview.test.tsx.snap | 4 +- .../components/shared/ManagedTable/index.tsx | 14 +- .../__snapshots__/Stackframe.test.tsx.snap | 4 + .../TransactionActionMenu.test.tsx.snap | 4 +- .../public/components/table/table.tsx | 4 +- .../components/table/table_type_configs.tsx | 2 +- .../public/pages/beat/details.tsx | 3 +- .../dropdown_filter.examples.storyshot | 10 ++ .../extended_template.examples.storyshot | 6 +- .../date_format.examples.storyshot | 9 +- .../number_format.examples.storyshot | 9 +- .../__snapshots__/asset.examples.storyshot | 24 ++- .../color_dot.examples.storyshot | 8 + .../color_manager.examples.storyshot | 24 ++- .../color_palette.examples.storyshot | 6 + .../color_picker.examples.storyshot | 28 +++- .../custom_element_modal.examples.storyshot | 30 +++- .../element_card.examples.storyshot | 6 + .../element_controls.examples.storyshot | 6 +- .../element_grid.examples.storyshot | 24 ++- .../file_upload.examples.storyshot | 3 +- .../font_picker.examples.storyshot | 6 +- .../item_grid.examples.storyshot | 12 ++ .../keyboard_shortcuts_doc.examples.storyshot | 3 +- .../sidebar_header.examples.storyshot | 12 +- .../__snapshots__/tag.examples.storyshot | 4 + .../__snapshots__/tag_list.examples.storyshot | 6 + .../pdf_panel.examples.storyshot | 3 +- .../workpad_export.examples.storyshot | 6 +- .../extended_template.examples.storyshot | 27 ++-- .../extended_template.examples.storyshot | 12 +- .../simple_template.examples.storyshot | 2 + .../__snapshots__/shareable.test.tsx.snap | 10 +- .../__snapshots__/canvas.examples.storyshot | 33 +++-- .../__tests__/__snapshots__/app.test.tsx.snap | 2 +- .../__snapshots__/footer.examples.storyshot | 22 ++- .../page_controls.examples.storyshot | 18 ++- .../__snapshots__/title.examples.storyshot | 6 + .../autoplay_settings.examples.storyshot | 12 ++ .../__snapshots__/settings.examples.storyshot | 6 +- .../toolbar_settings.examples.storyshot | 12 ++ .../__snapshots__/settings.test.tsx.snap | 104 ++++++++++--- .../extend_index_management.test.js.snap | 6 +- .../__snapshots__/policy_table.test.js.snap | 3 + .../node_attrs_details/node_attrs_details.js | 2 +- .../template_table/template_table.tsx | 8 +- .../components/nodes_overview/table.tsx | 6 +- .../sections/anomalies/table.tsx | 10 +- x-pack/legacy/plugins/infra/types/eui.d.ts | 23 --- .../__snapshots__/license_status.test.js.snap | 4 +- .../upload_license.test.tsx.snap | 48 ++++-- .../pipelines_table.test.js.snap | 1 - .../upgrade_failure.test.js.snap | 12 ++ .../annotations_table.test.js.snap | 1 - .../components/job_messages/job_messages.tsx | 5 +- .../components/ml_in_memory_table/types.ts | 29 ++-- .../__snapshots__/events_table.test.js.snap | 2 - .../table/__snapshots__/table.test.js.snap | 1 - .../list/__snapshots__/table.test.js.snap | 2 - .../__snapshots__/no_data.test.js.snap | 4 + .../__snapshots__/summary_status.test.js.snap | 4 + .../remote_cluster_form.test.js.snap | 9 ++ .../report_info_button.test.tsx.snap | 34 +++-- .../report_listing.test.tsx.snap | 140 ------------------ .../public/components/report_listing.tsx | 9 +- .../api_keys_grid_page.test.tsx.snap | 18 ++- .../components/api_keys_grid_page.tsx | 5 +- .../__snapshots__/feature_table.test.tsx.snap | 3 - .../privilege_space_table.tsx | 6 +- .../roles_grid_page.test.tsx.snap | 9 +- .../roles_grid/components/roles_grid_page.tsx | 9 +- .../users_grid/components/users_list_page.tsx | 10 +- .../event_details/event_fields_browser.tsx | 1 + .../components/event_details/helpers.tsx | 12 +- .../fields_browser/category_columns.tsx | 2 +- .../components/fields_browser/field_items.tsx | 4 +- .../ml/tables/anomalies_host_table.tsx | 4 +- .../ml/tables/anomalies_network_table.tsx | 4 +- .../components/ml/tables/basic_table.tsx | 11 +- .../ml_popover/jobs_table/jobs_table.tsx | 2 +- .../siem/public/components/notes/columns.tsx | 3 + .../siem/public/components/notes/helpers.tsx | 8 +- .../siem/public/components/notes/index.tsx | 16 +- .../open_timeline/timelines_table/index.tsx | 7 +- .../page/network/network_http_table/index.tsx | 3 +- .../components/paginated_table/index.tsx | 21 ++- .../__snapshots__/index.test.tsx.snap | 26 +++- .../plugins/siem/public/graphql/types.ts | 14 +- .../detection_engine/rule_details/index.tsx | 5 +- .../rules/activity_monitor/columns.tsx | 32 ++-- .../rules/activity_monitor/index.tsx | 21 +-- .../rules/all_rules/columns.tsx | 18 ++- .../rules/all_rules/index.tsx | 2 +- .../pages/detection_engine/rules/types.ts | 2 +- .../policy_list/policy_table/policy_table.tsx | 4 +- .../repository_table/repository_table.tsx | 4 +- .../restore_table/shards_table.tsx | 1 + .../snapshot_table/snapshot_table.tsx | 4 +- .../enabled_features/feature_table.tsx | 2 +- .../spaces_grid_pages.test.tsx.snap | 1 - .../components/step_define/pivot_preview.tsx | 2 +- .../components/transform_list/columns.tsx | 2 +- .../expanded_row_messages_pane.tsx | 4 +- .../expanded_row_preview_pane.tsx | 11 +- .../transform_list/transform_list.tsx | 9 +- .../tabs/checkup/deprecations/index_table.tsx | 4 +- .../monitor_status.bar.test.tsx.snap | 2 + .../uptime_date_picker.test.tsx.snap | 8 + .../__snapshots__/donut_chart.test.tsx.snap | 4 + .../__snapshots__/empty_state.test.tsx.snap | 12 ++ .../filter_popover.test.tsx.snap | 1 + .../functional/monitor_list/monitor_list.tsx | 8 +- .../functional/monitor_list/types.ts | 4 +- .../functional/ping_list/ping_list.tsx | 2 +- .../json_watch_edit_simulate_results.tsx | 6 +- .../watch_list/components/watch_list.tsx | 2 +- .../watch_status/components/watch_history.tsx | 2 +- x-pack/package.json | 2 +- yarn.lock | 8 +- 150 files changed, 997 insertions(+), 626 deletions(-) diff --git a/package.json b/package.json index a771a130d08b1..1cec7aaacbbc9 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "@elastic/charts": "^14.0.0", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "1.0.5", - "@elastic/eui": "17.0.0", + "@elastic/eui": "17.1.2", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap index 626c91b6a9668..9bd686776138f 100644 --- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap +++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap @@ -31,7 +31,7 @@ Array [ ] `; -exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -74,4 +74,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; diff --git a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap index 3928c54f90179..131ec836f5252 100644 --- a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap +++ b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap @@ -29,7 +29,7 @@ Array [ ] `; -exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
Modal content
"`; +exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
Modal content
"`; exports[`ModalService openModal() with a currently active modal replaces the current modal with a new one 1`] = ` Array [ diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap index 4ea658bcd03ef..178014a691be3 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -230,14 +230,18 @@ exports[`DashboardEmptyScreen renders correctly with visualize paragraph 1`] = ` type="dashboardApp" > `; exports[`Table should render the boolean template (true) 1`] = ` `; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap index f2a55649fe4d7..4716fb8f77633 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/components/table/__jest__/__snapshots__/table.test.js.snap @@ -55,7 +55,6 @@ exports[`Table should render normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap index 415bae7389e97..7856572373e79 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/source_filters_table/components/table/__jest__/__snapshots__/table.test.js.snap @@ -78,7 +78,6 @@ exports[`Table should render normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx index f54afc4a5e359..f254a6bc22a0d 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx @@ -59,7 +59,7 @@ const columns = [ ))} ), - dataType: 'string', + dataType: 'string' as const, sortable: ({ sort }: { sort: string }) => sort, }, ]; @@ -72,7 +72,7 @@ const pagination = { const sorting = { sort: { field: 'title', - direction: 'asc', + direction: 'asc' as const, }, }; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap index 843c8207c88c3..731a3379491c1 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap @@ -54,7 +54,6 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap index a9175e7b2a63e..ace06e0420a7c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/flyout/__jest__/__snapshots__/flyout.test.js.snap @@ -90,7 +90,6 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -116,7 +115,6 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` } } responsive={true} - sorting={false} /> @@ -411,7 +409,6 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -448,7 +445,6 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` } } responsive={true} - sorting={false} /> diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap index 6060e96f3cfb6..941a0ffded820 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/components/relationships/__jest__/__snapshots__/relationships.test.js.snap @@ -83,7 +83,6 @@ exports[`Relationships should render dashboards normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -155,7 +154,6 @@ exports[`Relationships should render dashboards normally 1`] = ` ], } } - sorting={false} />
@@ -294,7 +292,6 @@ exports[`Relationships should render index patterns normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -371,7 +368,6 @@ exports[`Relationships should render index patterns normally 1`] = ` ], } } - sorting={false} />
@@ -461,7 +457,6 @@ exports[`Relationships should render searches normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -538,7 +533,6 @@ exports[`Relationships should render searches normally 1`] = ` ], } } - sorting={false} />
@@ -628,7 +622,6 @@ exports[`Relationships should render visualizations normally 1`] = ` }, ] } - executeQueryOptions={Object {}} items={ Array [ Object { @@ -700,7 +693,6 @@ exports[`Relationships should render visualizations normally 1`] = ` ], } } - sorting={false} />
diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap index ca6b872c73f8f..0b44c7dc4e860 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -169,6 +169,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -226,6 +227,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -275,6 +277,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiBetaBadge__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -290,6 +293,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -325,6 +329,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -361,6 +366,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -494,6 +500,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -551,6 +558,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -600,6 +608,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiBetaBadge__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -615,6 +624,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -650,6 +660,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -686,6 +697,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -758,6 +770,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -815,6 +828,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiFormControlLayoutCustomIcon__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -864,6 +878,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--medium euiIcon-isLoading euiBetaBadge__icon" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -879,6 +894,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -914,6 +930,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -950,6 +967,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` class="euiIcon euiIcon--large euiIcon--secondary euiIcon-isLoading" focusable="false" height="16" + role="img" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg" @@ -1031,16 +1049,18 @@ exports[`NewVisModal filter for visualization types should render as expected 1` type="cross" >