From 790739b41bc468e7b4b534f220358a79398fb05f Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Thu, 14 Nov 2019 12:27:27 -0600 Subject: [PATCH 01/28] Remove duplicate but in error message (#50530) --- .../src/types/__snapshots__/uri_type.test.ts.snap | 4 ++-- packages/kbn-config-schema/src/types/uri_type.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap index 2836c80f5b5ad6..81fafdb4a7b339 100644 --- a/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap +++ b/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#scheme returns error when shorter string 1`] = `"expected URI with scheme [http|https] but but got [ftp://elastic.co]."`; +exports[`#scheme returns error when shorter string 1`] = `"expected URI with scheme [http|https] but got [ftp://elastic.co]."`; -exports[`#scheme returns error when shorter string 2`] = `"expected URI with scheme [http|https] but but got [file:///kibana.log]."`; +exports[`#scheme returns error when shorter string 2`] = `"expected URI with scheme [http|https] but got [file:///kibana.log]."`; exports[`#validate throws when returns string 1`] = `"validator failure"`; diff --git a/packages/kbn-config-schema/src/types/uri_type.ts b/packages/kbn-config-schema/src/types/uri_type.ts index f283554de527e3..df1ce9e869d3b4 100644 --- a/packages/kbn-config-schema/src/types/uri_type.ts +++ b/packages/kbn-config-schema/src/types/uri_type.ts @@ -36,7 +36,7 @@ export class URIType extends Type { case 'string.base': return `expected value of type [string] but got [${typeDetect(value)}].`; case 'string.uriCustomScheme': - return `expected URI with scheme [${scheme}] but but got [${value}].`; + return `expected URI with scheme [${scheme}] but got [${value}].`; case 'string.uri': return `value is [${value}] but it must be a valid URI (see RFC 3986).`; } From fef7be495e678be13f5f87b1a04f72bcab3cbce6 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 14 Nov 2019 10:32:35 -0800 Subject: [PATCH 02/28] [DOCS] Adds link to content security policy doc (#50698) --- docs/setup/settings.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 0f17ffcf26930e..11a50fea92f052 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -32,7 +32,8 @@ strongly recommend that you keep the default CSP rules that ship with Kibana. `csp.strict:`:: *Default: `true`* Blocks access to Kibana to any browser that does not enforce even rudimentary CSP rules. In practice, this will disable -support for older, less safe browsers like Internet Explorer. +support for older, less safe browsers like Internet Explorer. +See <> for more information. `csp.warnLegacyBrowsers:`:: *Default: `true`* Shows a warning message after loading Kibana to any browser that does not enforce even rudimentary CSP rules, From d7771285c75b8e96eb0a357df31a7151d1bb82cb Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 14 Nov 2019 13:44:00 -0500 Subject: [PATCH 03/28] [Task manager] Adds ensureScheduling api to allow safer rescheduling of existing tasks (#50232) Adds an ensureScheduling api to Task Manager which allow safer rescheduling of existing tasks by handling the case where a Task with a known ID is scheduled and clashes with an existing schedule of that same task. --- .../legacy/plugins/lens/server/usage/task.ts | 2 +- .../server/maps_telemetry/telemetry_task.js | 2 +- .../legacy/plugins/oss_telemetry/index.d.ts | 2 +- .../oss_telemetry/server/lib/tasks/index.ts | 2 +- .../plugins/oss_telemetry/test_utils/index.ts | 2 +- x-pack/legacy/plugins/task_manager/README.md | 11 +++ .../plugins/task_manager/plugin.test.ts | 1 + x-pack/legacy/plugins/task_manager/plugin.ts | 2 + x-pack/legacy/plugins/task_manager/task.ts | 19 +++++ .../plugins/task_manager/task_manager.mock.ts | 1 + .../plugins/task_manager/task_manager.test.ts | 79 +++++++++++++++++++ .../plugins/task_manager/task_manager.ts | 23 ++++++ .../plugins/task_manager/init_routes.js | 31 +++++--- .../task_manager/task_manager_integration.js | 28 ++++++- 14 files changed, 190 insertions(+), 15 deletions(-) diff --git a/x-pack/legacy/plugins/lens/server/usage/task.ts b/x-pack/legacy/plugins/lens/server/usage/task.ts index 3cb857a453e1d5..03e085cc9e669f 100644 --- a/x-pack/legacy/plugins/lens/server/usage/task.ts +++ b/x-pack/legacy/plugins/lens/server/usage/task.ts @@ -82,7 +82,7 @@ function scheduleTasks(server: Server) { // function block. (async () => { try { - await taskManager.schedule({ + await taskManager.ensureScheduled({ id: TASK_ID, taskType: TELEMETRY_TASK_TYPE, state: { byDate: {}, suggestionsByDate: {}, saved: {}, runs: 0 }, diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js b/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js index 3702bc8e29539d..78b04543e72f20 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/telemetry_task.js @@ -29,7 +29,7 @@ export function scheduleTask(server) { // function block. (async () => { try { - await taskManager.schedule({ + await taskManager.ensureScheduled({ id: TASK_ID, taskType: TELEMETRY_TASK_TYPE, state: { stats: {}, runs: 0 }, diff --git a/x-pack/legacy/plugins/oss_telemetry/index.d.ts b/x-pack/legacy/plugins/oss_telemetry/index.d.ts index 9f735c676fe6d2..012f9876273694 100644 --- a/x-pack/legacy/plugins/oss_telemetry/index.d.ts +++ b/x-pack/legacy/plugins/oss_telemetry/index.d.ts @@ -46,7 +46,7 @@ export interface HapiServer { }; task_manager: { registerTaskDefinitions: (opts: any) => void; - schedule: (opts: any) => Promise; + ensureScheduled: (opts: any) => Promise; fetch: ( opts: any ) => Promise<{ diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/index.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/index.ts index eaa8cc74058212..16e83a7938e60c 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/tasks/index.ts @@ -42,7 +42,7 @@ export function scheduleTasks(server: HapiServer) { // function block. (async () => { try { - await taskManager.schedule({ + await taskManager.ensureScheduled({ id: `${PLUGIN_ID}-${VIS_TELEMETRY_TASK}`, taskType: VIS_TELEMETRY_TASK, state: { stats: {}, runs: 0 }, diff --git a/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts b/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts index 7168f598dca237..998a1d2beeab14 100644 --- a/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/test_utils/index.ts @@ -50,7 +50,7 @@ export const getMockKbnServer = ( xpack_main: {}, task_manager: { registerTaskDefinitions: (opts: any) => undefined, - schedule: (opts: any) => Promise.resolve(), + ensureScheduled: (opts: any) => Promise.resolve(), fetch: mockTaskFetch, }, }, diff --git a/x-pack/legacy/plugins/task_manager/README.md b/x-pack/legacy/plugins/task_manager/README.md index 63c92102af251f..744643458e136d 100644 --- a/x-pack/legacy/plugins/task_manager/README.md +++ b/x-pack/legacy/plugins/task_manager/README.md @@ -222,6 +222,9 @@ The data stored for a task instance looks something like this: The task manager mixin exposes a taskManager object on the Kibana server which plugins can use to manage scheduled tasks. Each method takes an optional `scope` argument and ensures that only tasks with the specified scope(s) will be affected. +### schedule +Using `schedule` you can instruct TaskManger to schedule an instance of a TaskType at some point in the future. + ```js const taskManager = server.plugins.task_manager; // Schedules a task. All properties are as documented in the previous @@ -256,6 +259,14 @@ const results = await manager.find({ scope: 'my-fanci-app', searchAfter: ['ids'] } ``` +### ensureScheduling +When using the `schedule` api to schedule a Task you can provide a hard coded `id` on the Task. This tells TaskManager to use this `id` to identify the Task Instance rather than generate an `id` on its own. +The danger is that in such a situation, a Task with that same `id` might already have been scheduled at some earlier point, and this would result in an error. In some cases, this is the expected behavior, but often you only care about ensuring the task has been _scheduled_ and don't need it to be scheduled a fresh. + +To achieve this you should use the `ensureScheduling` api which has the exact same behavior as `schedule`, except it allows the scheduling of a Task with an `id` that's already in assigned to another Task and it will assume that the existing Task is the one you wished to `schedule`, treating this as a successful operation. + +### more options + More custom access to the tasks can be done directly via Elasticsearch, though that won't be officially supported, as we can change the document structure at any time. ## Middleware diff --git a/x-pack/legacy/plugins/task_manager/plugin.test.ts b/x-pack/legacy/plugins/task_manager/plugin.test.ts index f8ca6bd7a9ab37..4f2effb5da3a8f 100644 --- a/x-pack/legacy/plugins/task_manager/plugin.test.ts +++ b/x-pack/legacy/plugins/task_manager/plugin.test.ts @@ -42,6 +42,7 @@ describe('Task Manager Plugin', () => { expect(setupResult).toMatchInlineSnapshot(` Object { "addMiddleware": [Function], + "ensureScheduled": [Function], "fetch": [Function], "registerTaskDefinitions": [Function], "remove": [Function], diff --git a/x-pack/legacy/plugins/task_manager/plugin.ts b/x-pack/legacy/plugins/task_manager/plugin.ts index f8d95f4880c6e3..3e1514bd5234fd 100644 --- a/x-pack/legacy/plugins/task_manager/plugin.ts +++ b/x-pack/legacy/plugins/task_manager/plugin.ts @@ -11,6 +11,7 @@ export interface PluginSetupContract { fetch: TaskManager['fetch']; remove: TaskManager['remove']; schedule: TaskManager['schedule']; + ensureScheduled: TaskManager['ensureScheduled']; addMiddleware: TaskManager['addMiddleware']; registerTaskDefinitions: TaskManager['registerTaskDefinitions']; } @@ -59,6 +60,7 @@ export class Plugin { fetch: (...args) => taskManager.fetch(...args), remove: (...args) => taskManager.remove(...args), schedule: (...args) => taskManager.schedule(...args), + ensureScheduled: (...args) => taskManager.ensureScheduled(...args), addMiddleware: (...args) => taskManager.addMiddleware(...args), registerTaskDefinitions: (...args) => taskManager.registerTaskDefinitions(...args), }; diff --git a/x-pack/legacy/plugins/task_manager/task.ts b/x-pack/legacy/plugins/task_manager/task.ts index dd74acc2636e94..3eeb23685f3771 100644 --- a/x-pack/legacy/plugins/task_manager/task.ts +++ b/x-pack/legacy/plugins/task_manager/task.ts @@ -10,6 +10,20 @@ import Joi from 'joi'; * Type definitions and validations for tasks. */ +/** + * Require + * @desc Create a Subtype of type T `T` such that the property under key `P` becomes required + * @example + * type TaskInstance = { + * id?: string; + * name: string; + * }; + * + * // This type is now defined as { id: string; name: string; } + * type TaskInstanceWithId = Require; + */ +type Require = Omit & Required>; + /** * A loosely typed definition of the elasticjs wrapper. It's beyond the scope * of this work to try to make a comprehensive type definition of this. @@ -216,6 +230,11 @@ export interface TaskInstance { ownerId?: string | null; } +/** + * A task instance that has an id. + */ +export type TaskInstanceWithId = Require; + /** * A task instance that has an id and is ready for storage. */ diff --git a/x-pack/legacy/plugins/task_manager/task_manager.mock.ts b/x-pack/legacy/plugins/task_manager/task_manager.mock.ts index 2737e83f0ba4a8..515099a8bd4795 100644 --- a/x-pack/legacy/plugins/task_manager/task_manager.mock.ts +++ b/x-pack/legacy/plugins/task_manager/task_manager.mock.ts @@ -10,6 +10,7 @@ const createTaskManagerMock = () => { const mocked: jest.Mocked = { registerTaskDefinitions: jest.fn(), addMiddleware: jest.fn(), + ensureScheduled: jest.fn(), schedule: jest.fn(), fetch: jest.fn(), remove: jest.fn(), diff --git a/x-pack/legacy/plugins/task_manager/task_manager.test.ts b/x-pack/legacy/plugins/task_manager/task_manager.test.ts index 9ae2f5e1e027b5..0b4a22910e6111 100644 --- a/x-pack/legacy/plugins/task_manager/task_manager.test.ts +++ b/x-pack/legacy/plugins/task_manager/task_manager.test.ts @@ -121,6 +121,85 @@ describe('TaskManager', () => { expect(savedObjectsClient.create).toHaveBeenCalled(); }); + test('allows scheduling existing tasks that may have already been scheduled', async () => { + const client = new TaskManager(taskManagerOpts); + client.registerTaskDefinitions({ + foo: { + type: 'foo', + title: 'Foo', + createTaskRunner: jest.fn(), + }, + }); + savedObjectsClient.create.mockRejectedValueOnce({ + statusCode: 409, + }); + + client.start(); + + const result = await client.ensureScheduled({ + id: 'my-foo-id', + taskType: 'foo', + params: {}, + state: {}, + }); + + expect(result.id).toEqual('my-foo-id'); + }); + + test('doesnt ignore failure to scheduling existing tasks for reasons other than already being scheduled', async () => { + const client = new TaskManager(taskManagerOpts); + client.registerTaskDefinitions({ + foo: { + type: 'foo', + title: 'Foo', + createTaskRunner: jest.fn(), + }, + }); + savedObjectsClient.create.mockRejectedValueOnce({ + statusCode: 500, + }); + + client.start(); + + return expect( + client.ensureScheduled({ + id: 'my-foo-id', + taskType: 'foo', + params: {}, + state: {}, + }) + ).rejects.toMatchObject({ + statusCode: 500, + }); + }); + + test('doesnt allow naively rescheduling existing tasks that have already been scheduled', async () => { + const client = new TaskManager(taskManagerOpts); + client.registerTaskDefinitions({ + foo: { + type: 'foo', + title: 'Foo', + createTaskRunner: jest.fn(), + }, + }); + savedObjectsClient.create.mockRejectedValueOnce({ + statusCode: 409, + }); + + client.start(); + + return expect( + client.schedule({ + id: 'my-foo-id', + taskType: 'foo', + params: {}, + state: {}, + }) + ).rejects.toMatchObject({ + statusCode: 409, + }); + }); + test('allows and queues removing tasks before starting', async () => { const client = new TaskManager(taskManagerOpts); savedObjectsClient.delete.mockResolvedValueOnce({}); diff --git a/x-pack/legacy/plugins/task_manager/task_manager.ts b/x-pack/legacy/plugins/task_manager/task_manager.ts index 4ddb18c7cfe748..269d7ff67384b4 100644 --- a/x-pack/legacy/plugins/task_manager/task_manager.ts +++ b/x-pack/legacy/plugins/task_manager/task_manager.ts @@ -15,6 +15,7 @@ import { TaskDictionary, ConcreteTaskInstance, RunContext, + TaskInstanceWithId, TaskInstance, } from './task'; import { TaskPoller } from './task_poller'; @@ -29,6 +30,8 @@ import { } from './task_store'; import { identifyEsError } from './lib/identify_es_error'; +const VERSION_CONFLICT_STATUS = 409; + export interface TaskManagerOpts { logger: Logger; config: any; @@ -219,6 +222,26 @@ export class TaskManager { return result; } + /** + * Schedules a task with an Id + * + * @param task - The task being scheduled. + * @returns {Promise} + */ + public async ensureScheduled( + taskInstance: TaskInstanceWithId, + options?: any + ): Promise { + try { + return await this.schedule(taskInstance, options); + } catch (err) { + if (err.statusCode === VERSION_CONFLICT_STATUS) { + return taskInstance; + } + throw err; + } + } + /** * Fetches a paginatable list of scheduled tasks. * diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js index ad06fb15fd9aee..a9dfabae6d609f 100644 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js +++ b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js @@ -32,21 +32,34 @@ export function initRoutes(server) { config: { validate: { payload: Joi.object({ - taskType: Joi.string().required(), - interval: Joi.string().optional(), - params: Joi.object().required(), - state: Joi.object().optional(), - id: Joi.string().optional(), + task: Joi.object({ + taskType: Joi.string().required(), + interval: Joi.string().optional(), + params: Joi.object().required(), + state: Joi.object().optional(), + id: Joi.string().optional() + }), + ensureScheduled: Joi.boolean() + .default(false) + .optional(), }), }, }, async handler(request) { try { - const task = await taskManager.schedule({ - ...request.payload, + const { ensureScheduled = false, task: taskFields } = request.payload; + const task = { + ...taskFields, scope: [scope], - }, { request }); - return task; + }; + + const taskResult = await ( + ensureScheduled + ? taskManager.ensureScheduled(task, { request }) + : taskManager.schedule(task, { request }) + ); + + return taskResult; } catch (err) { return err; } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index 30d830cd6c919d..07877f3c09d847 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -60,7 +60,15 @@ export default function ({ getService }) { function scheduleTask(task) { return supertest.post('/api/sample_tasks') .set('kbn-xsrf', 'xxx') - .send(task) + .send({ task }) + .expect(200) + .then((response) => response.body); + } + + function scheduleTaskIfNotExists(task) { + return supertest.post('/api/sample_tasks') + .set('kbn-xsrf', 'xxx') + .send({ task, ensureScheduled: true }) .expect(200) .then((response) => response.body); } @@ -116,6 +124,24 @@ export default function ({ getService }) { expect(result.id).to.be('test-task-for-sample-task-plugin-to-test-task-manager'); }); + it('should allow a task with a given ID to be scheduled multiple times', async () => { + const result = await scheduleTaskIfNotExists({ + id: 'test-task-to-reschedule-in-task-manager', + taskType: 'sampleTask', + params: { }, + }); + + expect(result.id).to.be('test-task-to-reschedule-in-task-manager'); + + const rescheduleResult = await scheduleTaskIfNotExists({ + id: 'test-task-to-reschedule-in-task-manager', + taskType: 'sampleTask', + params: { }, + }); + + expect(rescheduleResult.id).to.be('test-task-to-reschedule-in-task-manager'); + }); + it('should reschedule if task errors', async () => { const task = await scheduleTask({ taskType: 'sampleTask', From 73697c1777678517bff8491f3a9a4af7c6caef45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 14 Nov 2019 13:52:59 -0500 Subject: [PATCH 04/28] [Logs UI] Improve infra plugin compatibility with TS 3.7 (#50491) This prepares the `infra` plugin code to be compatible with TypeScript 3.7. It should not have any user-facing changes. --- .../eui_styled_components.tsx | 2 +- .../public/components/help_center_content.tsx | 22 +++++++++++-------- .../containers/waffle/with_waffle_options.tsx | 8 ++++--- .../infra/public/utils/typed_react.tsx | 10 +++++---- .../server/lib/snapshot/response_helpers.ts | 12 ++++++++-- 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx b/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx index 8e5fba31ac5a44..8becf6892ff92c 100644 --- a/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx +++ b/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx @@ -38,6 +38,6 @@ const { injectGlobal, keyframes, withTheme, -} = styledComponents as ThemedStyledComponentsModule; +} = (styledComponents as unknown) as ThemedStyledComponentsModule; export { css, euiStyled, EuiThemeProvider, injectGlobal, keyframes, withTheme }; diff --git a/x-pack/legacy/plugins/infra/public/components/help_center_content.tsx b/x-pack/legacy/plugins/infra/public/components/help_center_content.tsx index d583b22f65357c..0560c42d7498b0 100644 --- a/x-pack/legacy/plugins/infra/public/components/help_center_content.tsx +++ b/x-pack/legacy/plugins/infra/public/components/help_center_content.tsx @@ -5,7 +5,7 @@ */ import { EuiLink } from '@elastic/eui'; -import React from 'react'; +import React, { useEffect } from 'react'; import ReactDOM from 'react-dom'; import chrome from 'ui/chrome'; @@ -20,17 +20,21 @@ const Content: React.FC = ({ feedbackLink, feedbackLinkT ); -export class HelpCenterContent extends React.Component { - public componentDidMount = () => { +export const HelpCenterContent: React.FC = ({ + feedbackLink, + feedbackLinkText, +}) => { + useEffect(() => { chrome.helpExtension.set(domElement => { - ReactDOM.render(, domElement); + ReactDOM.render( + , + domElement + ); return () => { ReactDOM.unmountComponentAtNode(domElement); }; }); - }; + }, [feedbackLink, feedbackLinkText]); - public render = () => { - return null; - }; -} + return null; +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/waffle/with_waffle_options.tsx b/x-pack/legacy/plugins/infra/public/containers/waffle/with_waffle_options.tsx index 60de905122876f..2e3e1078e1f627 100644 --- a/x-pack/legacy/plugins/infra/public/containers/waffle/with_waffle_options.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/waffle/with_waffle_options.tsx @@ -90,7 +90,7 @@ export const WithWaffleOptionsUrlState = () => ( changeAutoBounds, changeBoundsOverride, }) => ( - urlState={urlState} urlStateKey="waffleOptions" mapToUrlState={mapToUrlState} @@ -158,8 +158,10 @@ const mapToUrlState = (value: any): WaffleOptionsUrlState | undefined => } : undefined; +const isInfraNodeType = (value: any): value is InfraNodeType => value in InfraNodeType; + const isInfraSnapshotMetricInput = (subject: any): subject is InfraSnapshotMetricInput => { - return subject != null && subject.type != null && InfraSnapshotMetricType[subject.type] != null; + return subject != null && subject.type in InfraSnapshotMetricType; }; const isInfraSnapshotGroupbyInput = (subject: any): subject is InfraSnapshotGroupbyInput => { @@ -181,7 +183,7 @@ const mapToGroupByUrlState = (subject: any) => { }; const mapToNodeTypeUrlState = (subject: any) => { - return subject && InfraNodeType[subject] ? subject : undefined; + return isInfraNodeType(subject) ? subject : undefined; }; const mapToViewUrlState = (subject: any) => { diff --git a/x-pack/legacy/plugins/infra/public/utils/typed_react.tsx b/x-pack/legacy/plugins/infra/public/utils/typed_react.tsx index d78a8f6f7a48db..eadf3eb9686b55 100644 --- a/x-pack/legacy/plugins/infra/public/utils/typed_react.tsx +++ b/x-pack/legacy/plugins/infra/public/utils/typed_react.tsx @@ -11,18 +11,18 @@ import { InferableComponentEnhancerWithProps } from 'react-redux'; export type RendererResult = React.ReactElement | null; export type RendererFunction = (args: RenderArgs) => Result; -export type ChildFunctionRendererProps = { +export type ChildFunctionRendererProps = { children: RendererFunction; initializeOnMount?: boolean; resetOnUnmount?: boolean; } & RenderArgs; -interface ChildFunctionRendererOptions { +interface ChildFunctionRendererOptions { onInitialize?: (props: RenderArgs) => void; onCleanup?: (props: RenderArgs) => void; } -export const asChildFunctionRenderer = ( +export const asChildFunctionRenderer = ( hoc: InferableComponentEnhancerWithProps, { onInitialize, onCleanup }: ChildFunctionRendererOptions = {} ) => @@ -43,7 +43,9 @@ export const asChildFunctionRenderer = ( } public render() { - return this.props.children(this.getRendererArgs()); + return (this.props.children as ChildFunctionRendererProps['children'])( + this.getRendererArgs() + ); } private getRendererArgs = () => diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/response_helpers.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/response_helpers.ts index 6655e1272b6791..6b18d9489c100b 100644 --- a/x-pack/legacy/plugins/infra/server/lib/snapshot/response_helpers.ts +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/response_helpers.ts @@ -73,11 +73,19 @@ export const getIPFromBucket = ( nodeType: InfraNodeType, bucket: InfraSnapshotNodeGroupByBucket ): string | null => { - const ip = get(bucket, `ip.hits.hits[0]._source.${IP_FIELDS[nodeType]}`, null); + const ip = get( + bucket, + `ip.hits.hits[0]._source.${IP_FIELDS[nodeType]}`, + null + ); + if (Array.isArray(ip)) { return ip.find(isIPv4) || null; + } else if (typeof ip === 'string') { + return ip; } - return ip; + + return null; }; export const getNodePath = ( From 930c156585332d461aa8a84f54fcf2512bfb6f5b Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 14 Nov 2019 11:24:53 -0800 Subject: [PATCH 05/28] [DOCS] Adds note about backups to Upgrade doc (#50525) * [DOCS] Adds note about backups to Upgrade doc * [DOCS] Incorporates review comments in upgrade doc * [DOCS] Fixes typo --- docs/setup/upgrade.asciidoc | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc index ce0259c690b821..8c03032bb8ac36 100644 --- a/docs/setup/upgrade.asciidoc +++ b/docs/setup/upgrade.asciidoc @@ -2,7 +2,15 @@ == Upgrading {kib} Depending on the {kib} version you're upgrading from, the upgrade process to 7.0 -varies. +varies. + +NOTE: {kib} upgrades automatically when starting a new version, as described in +<>. +Although you do not need to manually back up {kib} before upgrading, we recommend +that you have a backup on hand. You can use +<> to back up {kib} +data by targeting `.kibana*` indices. If you are using the Reporting plugin, +you can also target `.reporting*` indices. [float] [[upgrade-before-you-begin]] @@ -12,7 +20,7 @@ Before you upgrade {kib}: * Consult the <>. * Before you upgrade production servers, test the upgrades in a dev environment. -* Backup your data with {es} {ref}/modules-snapshots.html[snapshots]. +* Back up your data with {es} {ref}/modules-snapshots.html[snapshots]. To roll back to an earlier version, you **must** have a backup of your data. * If you are using custom plugins, check that a compatible version is available. From 08471cc88a35e7fccffe93ce4372ca18d4f2507d Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 14 Nov 2019 12:56:15 -0700 Subject: [PATCH 06/28] [Reporting] Remove any types and references to Hapi (#49250) * [Reporting] Remove any types and references to Hapi * clarification comment * fix import --- .../reporting/export_types/png/types.d.ts | 2 +- .../browsers/download/ensure_downloaded.ts | 12 +--- .../reporting/server/browsers/install.ts | 3 +- .../reporting/server/browsers/types.d.ts | 14 +++++ .../reporting/server/lib/create_worker.ts | 14 ++++- .../reporting/server/lib/validate/index.ts | 10 ++- .../server/lib/validate/validate_config.ts | 10 ++- .../validate/validate_max_content_length.ts | 6 +- .../server/routes/generate_from_jobparams.ts | 5 +- .../plugins/reporting/server/routes/index.ts | 2 +- .../server/routes/lib/get_document_payload.ts | 39 +++++++----- .../reporting/server/routes/types.d.ts | 6 +- .../server/usage/get_reporting_usage.ts | 11 ++-- .../usage/get_reporting_usage_collector.ts | 8 ++- .../plugins/reporting/server/usage/types.d.ts | 10 +-- x-pack/legacy/plugins/reporting/types.d.ts | 62 +++++++++++++------ 16 files changed, 135 insertions(+), 79 deletions(-) diff --git a/x-pack/legacy/plugins/reporting/export_types/png/types.d.ts b/x-pack/legacy/plugins/reporting/export_types/png/types.d.ts index c7ea2bdfba59fa..895c1fa91d28fc 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/types.d.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/types.d.ts @@ -21,7 +21,7 @@ export interface JobDocPayloadPNG extends JobDocPayload { basePath?: string; browserTimezone: string; forceNow?: string; - layout: any; + layout: LayoutInstance; relativeUrl: string; objects: undefined; } diff --git a/x-pack/legacy/plugins/reporting/server/browsers/download/ensure_downloaded.ts b/x-pack/legacy/plugins/reporting/server/browsers/download/ensure_downloaded.ts index c79d2c263b2e19..73186966e3d2f3 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/download/ensure_downloaded.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/download/ensure_downloaded.ts @@ -8,7 +8,7 @@ import { resolve as resolvePath } from 'path'; import { existsSync } from 'fs'; import { chromium } from '../index'; -import { BrowserType } from '../types'; +import { BrowserDownload, BrowserType } from '../types'; import { md5 } from './checksum'; import { asyncMap } from './util'; @@ -40,15 +40,7 @@ export async function ensureAllBrowsersDownloaded() { * @param {BrowserSpec} browsers * @return {Promise} */ -async function ensureDownloaded( - browsers: Array<{ - paths: { - archivesPath: string; - baseUrl: string; - packages: Array<{ archiveFilename: string; archiveChecksum: string }>; - }; - }> -) { +async function ensureDownloaded(browsers: BrowserDownload[]) { await asyncMap(browsers, async browser => { const { archivesPath } = browser.paths; diff --git a/x-pack/legacy/plugins/reporting/server/browsers/install.ts b/x-pack/legacy/plugins/reporting/server/browsers/install.ts index 0f2ab28d2b75ec..6f099c36e69f21 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/install.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/install.ts @@ -12,6 +12,7 @@ import { LevelLogger as Logger } from '../lib/level_logger'; import { extract } from './extract'; // @ts-ignore import { md5 } from './download/checksum'; +import { BrowserDownload } from './types'; const chmod = promisify(fs.chmod); @@ -28,7 +29,7 @@ interface PathResponse { */ export async function installBrowser( logger: Logger, - browser: any, + browser: BrowserDownload, installsPath: string ): Promise { const pkg = browser.paths.packages.find((p: Package) => p.platforms.includes(process.platform)); diff --git a/x-pack/legacy/plugins/reporting/server/browsers/types.d.ts b/x-pack/legacy/plugins/reporting/server/browsers/types.d.ts index 43cbb0f13a5a7f..0c480fc82752bc 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/types.d.ts @@ -5,3 +5,17 @@ */ export type BrowserType = 'chromium'; + +export interface BrowserDownload { + paths: { + archivesPath: string; + baseUrl: string; + packages: Array<{ + archiveChecksum: string; + archiveFilename: string; + binaryChecksum: string; + binaryRelativePath: string; + platforms: string[]; + }>; + }; +} diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts index 7166659487c9bd..1cfc967cb31d1e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts @@ -5,6 +5,7 @@ */ import { PLUGIN_ID } from '../../common/constants'; +import { CancellationToken } from '../../common/cancellation_token'; import { ESQueueInstance, QueueConfig, @@ -14,6 +15,7 @@ import { JobDoc, JobDocPayload, JobSource, + RequestFacade, ServerFacade, } from '../../types'; // @ts-ignore untyped dependency @@ -39,17 +41,23 @@ function createWorkerFn(server: ServerFacade) { jobExecutors.set(exportType.jobType, executeJobFactory); } - const workerFn = (job: JobSource, jobdoc: JobDocPayload | JobDoc, cancellationToken?: any) => { + const workerFn = ( + job: JobSource, + arg1: JobDocPayload | JobDoc, + arg2: CancellationToken | RequestFacade | undefined + ) => { // pass the work to the jobExecutor if (!jobExecutors.get(job._source.jobtype)) { throw new Error(`Unable to find a job executor for the claimed job: [${job._id}]`); } + // job executor function signature is different depending on whether it + // is ESQueueWorkerExecuteFn or ImmediateExecuteFn if (job._id) { const jobExecutor = jobExecutors.get(job._source.jobtype) as ESQueueWorkerExecuteFn; - return jobExecutor(job._id, jobdoc as JobDoc, cancellationToken); + return jobExecutor(job._id, arg1 as JobDoc, arg2 as CancellationToken); } else { const jobExecutor = jobExecutors.get(job._source.jobtype) as ImmediateExecuteFn; - return jobExecutor(null, jobdoc as JobDocPayload, cancellationToken); + return jobExecutor(null, arg1 as JobDocPayload, arg2 as RequestFacade); } }; const workerOptions = { diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts index e0382c02053456..672f90358aba49 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/index.ts @@ -5,16 +5,20 @@ */ import { ServerFacade, Logger } from '../../../types'; +import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_factory'; import { validateBrowser } from './validate_browser'; import { validateConfig } from './validate_config'; import { validateMaxContentLength } from './validate_max_content_length'; -export async function runValidations(server: ServerFacade, logger: Logger, browserFactory: any) { +export async function runValidations( + server: ServerFacade, + logger: Logger, + browserFactory: HeadlessChromiumDriverFactory +) { try { - const config = server.config(); await Promise.all([ validateBrowser(server, browserFactory, logger), - validateConfig(config, logger), + validateConfig(server, logger), validateMaxContentLength(server, logger), ]); logger.debug(`Reporting plugin self-check ok!`); diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_config.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_config.ts index ad6c1e08368c7d..a1eb7be6ecae42 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_config.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_config.ts @@ -5,15 +5,19 @@ */ import crypto from 'crypto'; -import { Logger } from '../../../types'; +import { ServerFacade, Logger } from '../../../types'; + +export function validateConfig(serverFacade: ServerFacade, logger: Logger) { + const config = serverFacade.config(); -export function validateConfig(config: any, logger: Logger) { const encryptionKey = config.get('xpack.reporting.encryptionKey'); if (encryptionKey == null) { logger.warning( `Generating a random key for xpack.reporting.encryptionKey. To prevent pending reports from failing on restart, please set ` + `xpack.reporting.encryptionKey in kibana.yml` ); - config.set('xpack.reporting.encryptionKey', crypto.randomBytes(16).toString('hex')); + + // @ts-ignore: No set() method on KibanaConfig, just get() and has() + config.set('xpack.reporting.encryptionKey', crypto.randomBytes(16).toString('hex')); // update config in memory to contain a usable encryption key } } diff --git a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts index 01a8bfcf9b2826..ca38ce5d635c6a 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/validate/validate_max_content_length.ts @@ -5,12 +5,12 @@ */ import numeral from '@elastic/numeral'; import { defaults, get } from 'lodash'; -import { Logger } from '../../../types'; +import { Logger, ServerFacade } from '../../../types'; const KIBANA_MAX_SIZE_BYTES_PATH = 'xpack.reporting.csv.maxSizeBytes'; const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; -export async function validateMaxContentLength(server: any, logger: Logger) { +export async function validateMaxContentLength(server: ServerFacade, logger: Logger) { const config = server.config(); const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('data'); @@ -22,7 +22,7 @@ export async function validateMaxContentLength(server: any, logger: Logger) { const elasticSearchMaxContent = get(elasticClusterSettings, 'http.max_content_length', '100mb'); const elasticSearchMaxContentBytes = numeral().unformat(elasticSearchMaxContent.toUpperCase()); - const kibanaMaxContentBytes = config.get(KIBANA_MAX_SIZE_BYTES_PATH); + const kibanaMaxContentBytes: number = config.get(KIBANA_MAX_SIZE_BYTES_PATH); if (kibanaMaxContentBytes > elasticSearchMaxContentBytes) { logger.warning( diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts index 9c7e0a5f27786d..128cc44db4dc48 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -76,7 +76,10 @@ export function registerGenerateFromJobParams( const { exportType } = request.params; let response; try { - const jobParams = rison.decode(jobParamsRison); + const jobParams = rison.decode(jobParamsRison) as object | null; + if (!jobParams) { + throw new Error('missing jobParams!'); + } response = await handler(exportType, jobParams, request, h); } catch (err) { throw boom.badRequest(`invalid rison: ${jobParamsRison}`); diff --git a/x-pack/legacy/plugins/reporting/server/routes/index.ts b/x-pack/legacy/plugins/reporting/server/routes/index.ts index 118d6b3d43c17a..fd86b281ccf8a9 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/index.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/index.ts @@ -26,7 +26,7 @@ export function registerRoutes(server: ServerFacade, logger: Logger) { */ async function handler( exportTypeId: string, - jobParams: any, + jobParams: object, request: RequestFacade, h: ReportingResponseToolkit ) { diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts index 0b00516957f4b2..d3e9981a62b6ec 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -4,9 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import * as _ from 'lodash'; // @ts-ignore import contentDisposition from 'content-disposition'; -import * as _ from 'lodash'; +import { + ServerFacade, + ExportTypeDefinition, + JobDocExecuted, + JobDocOutputExecuted, +} from '../../../types'; import { oncePerServer } from '../../lib/once_per_server'; import { CSV_JOB_TYPE } from '../../../common/constants'; @@ -16,10 +22,10 @@ interface ICustomHeaders { const DEFAULT_TITLE = 'report'; -const getTitle = (exportType: any, title?: string): string => +const getTitle = (exportType: ExportTypeDefinition, title?: string): string => `${title || DEFAULT_TITLE}.${exportType.jobContentExtension}`; -const getReportingHeaders = (output: any, exportType: any) => { +const getReportingHeaders = (output: JobDocOutputExecuted, exportType: ExportTypeDefinition) => { const metaDataHeaders: ICustomHeaders = {}; if (exportType.jobType === CSV_JOB_TYPE) { @@ -33,20 +39,22 @@ const getReportingHeaders = (output: any, exportType: any) => { return metaDataHeaders; }; -function getDocumentPayloadFn(server: any) { - const exportTypesRegistry = server.plugins.reporting.exportTypesRegistry; +function getDocumentPayloadFn(server: ServerFacade) { + const exportTypesRegistry = server.plugins.reporting!.exportTypesRegistry; - function encodeContent(content: string, exportType: any) { + function encodeContent(content: string | null, exportType: ExportTypeDefinition) { switch (exportType.jobContentEncoding) { case 'base64': - return Buffer.from(content, 'base64'); + return content ? Buffer.from(content, 'base64') : content; // Buffer.from rejects null default: return content; } } - function getCompleted(output: any, jobType: string, title: any) { - const exportType = exportTypesRegistry.get((item: any) => item.jobType === jobType); + function getCompleted(output: JobDocOutputExecuted, jobType: string, title: string) { + const exportType = exportTypesRegistry.get( + (item: ExportTypeDefinition) => item.jobType === jobType + ); const filename = getTitle(exportType, title); const headers = getReportingHeaders(output, exportType); @@ -61,7 +69,7 @@ function getDocumentPayloadFn(server: any) { }; } - function getFailure(output: any) { + function getFailure(output: JobDocOutputExecuted) { return { statusCode: 500, content: { @@ -72,19 +80,18 @@ function getDocumentPayloadFn(server: any) { }; } - function getIncomplete(status: any) { + function getIncomplete(status: string) { return { statusCode: 503, content: status, contentType: 'application/json', - headers: { - 'retry-after': 30, - }, + headers: { 'retry-after': 30 }, }; } - return function getDocumentPayload(doc: any) { - const { status, output, jobtype: jobType, payload: { title } = { title: '' } } = doc._source; + return function getDocumentPayload(doc: { _source: JobDocExecuted }) { + const { status, jobtype: jobType, payload: { title } = { title: '' } } = doc._source; + const { output } = doc._source as { output: JobDocOutputExecuted }; if (status === 'completed') { return getCompleted(output, jobType, title); diff --git a/x-pack/legacy/plugins/reporting/server/routes/types.d.ts b/x-pack/legacy/plugins/reporting/server/routes/types.d.ts index 0b6a9708c7d1ac..aabe4445f9baec 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/types.d.ts @@ -7,13 +7,13 @@ import { RequestFacade, ReportingResponseToolkit, JobDocPayload } from '../../types'; export type HandlerFunction = ( - exportType: any, - jobParams: any, + exportType: string, + jobParams: object, request: RequestFacade, h: ReportingResponseToolkit ) => any; -export type HandlerErrorFunction = (exportType: any, err: Error) => any; +export type HandlerErrorFunction = (exportType: string, err: Error) => any; export interface QueuedJobPayload { error?: boolean; diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts index 69be64580ff5f7..0c85d39ae55d3e 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts @@ -5,6 +5,7 @@ */ import { get } from 'lodash'; +import { ServerFacade, ESCallCluster } from '../../types'; import { AggregationBuckets, AggregationResults, @@ -13,7 +14,6 @@ import { KeyCountBucket, RangeAggregationResults, RangeStats, - UsageObject, } from './types'; import { decorateRangeStats } from './decorate_range_stats'; // @ts-ignore untyped module @@ -80,7 +80,10 @@ type RangeStatSets = Partial< last7Days: RangeStats; } >; -async function handleResponse(server: any, response: AggregationResults): Promise { +async function handleResponse( + server: ServerFacade, + response: AggregationResults +): Promise { const buckets = get(response, 'aggregations.ranges.buckets'); if (!buckets) { return {}; @@ -98,7 +101,7 @@ async function handleResponse(server: any, response: AggregationResults): Promis }; } -export async function getReportingUsage(server: any, callCluster: any) { +export async function getReportingUsage(server: ServerFacade, callCluster: ESCallCluster) { const config = server.config(); const reportingIndex = config.get('xpack.reporting.index'); @@ -135,7 +138,7 @@ export async function getReportingUsage(server: any, callCluster: any) { return callCluster('search', params) .then((response: AggregationResults) => handleResponse(server, response)) - .then(async (usage: UsageObject) => { + .then(async (usage: RangeStatSets) => { // Allow this to explicitly throw an exception if/when this config is deprecated, // because we shouldn't collect browserType in that case! const browserType = config.get('xpack.reporting.capture.browser.type'); diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts index 0e1e0f36ab94ab..5c521937690574 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage_collector.ts @@ -6,26 +6,28 @@ // @ts-ignore untyped module import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants'; +import { ServerFacade, ESCallCluster } from '../../types'; import { KIBANA_REPORTING_TYPE } from '../../common/constants'; import { getReportingUsage } from './get_reporting_usage'; +import { RangeStats } from './types'; /* * @param {Object} server * @return {Object} kibana usage stats type collection object */ -export function getReportingUsageCollector(server: any, isReady: () => boolean) { +export function getReportingUsageCollector(server: ServerFacade, isReady: () => boolean) { const { collectorSet } = server.usage; return collectorSet.makeUsageCollector({ type: KIBANA_REPORTING_TYPE, isReady, - fetch: (callCluster: any) => getReportingUsage(server, callCluster), + fetch: (callCluster: ESCallCluster) => getReportingUsage(server, callCluster), /* * Format the response data into a model for internal upload * 1. Make this data part of the "kibana_stats" type * 2. Organize the payload in the usage.xpack.reporting namespace of the data payload */ - formatForBulkUpload: (result: any) => { + formatForBulkUpload: (result: RangeStats) => { return { type: KIBANA_STATS_TYPE_MONITORING, payload: { diff --git a/x-pack/legacy/plugins/reporting/server/usage/types.d.ts b/x-pack/legacy/plugins/reporting/server/usage/types.d.ts index 311e750fb6cc99..98e025ccf661ee 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/types.d.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -interface AvailableTotal { +export interface AvailableTotal { available: boolean; total: number; } @@ -56,13 +56,5 @@ export type RangeStats = JobTypes & { status: StatusCounts; }; -export type UsageObject = RangeStats & { - available: boolean; - enabled: boolean; - browser_type: string; - lastDay: RangeStats; - last7Days: RangeStats; -}; - export type ExportType = 'csv' | 'printable_pdf' | 'PNG'; export type FeatureAvailabilityMap = { [F in ExportType]: boolean }; diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index a34943786ac529..6d2808c5b560dc 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -8,7 +8,11 @@ import { ResponseObject } from 'hapi'; import { EventEmitter } from 'events'; import { Legacy } from 'kibana'; import { XPackMainPlugin } from '../xpack_main/xpack_main'; -import { ElasticsearchPlugin } from '../../../../src/legacy/core_plugins/elasticsearch'; +import { + ElasticsearchPlugin, + CallCluster, +} from '../../../../src/legacy/core_plugins/elasticsearch'; +import { CancellationToken } from './common/cancellation_token'; import { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; import { BrowserType } from './server/browsers/types'; @@ -18,9 +22,11 @@ export interface ReportingPlugin { queue: { addJob: (type: string, payload: object, options: object) => Job; }; + // TODO: convert exportTypesRegistry to TS exportTypesRegistry: { getById: (id: string) => ExportTypeDefinition; getAll: () => ExportTypeDefinition[]; + get: (callback: (item: ExportTypeDefinition) => boolean) => ExportTypeDefinition; }; browserDriverFactory: HeadlessChromiumDriverFactory; } @@ -52,18 +58,6 @@ export interface NetworkPolicy { rules: NetworkPolicyRule[]; } -// Tracks which parts of the legacy plugin system are being used -export type ReportingPluginSpecOptions = Legacy.PluginSpecOptions; - -export type ServerFacade = Legacy.Server & { - plugins: { - reporting?: ReportingPlugin; - xpack_main?: XPackMainPlugin & { - status?: any; - }; - }; -}; - interface ListQuery { page: string; size: string; @@ -79,7 +73,21 @@ interface DownloadParams { docId: string; } -// Tracks which parts of the legacy plugin system are being used +/* + * Legacy System + */ + +export type ReportingPluginSpecOptions = Legacy.PluginSpecOptions; + +export type ServerFacade = Legacy.Server & { + plugins: { + reporting?: ReportingPlugin; + xpack_main?: XPackMainPlugin & { + status?: any; + }; + }; +}; + interface ReportingRequest { query: ListQuery & GenerateQuery; params: DownloadParams; @@ -100,6 +108,12 @@ export type ResponseFacade = ResponseObject & { export type ReportingResponseToolkit = Legacy.ResponseToolkit; +export type ESCallCluster = CallCluster; + +/* + * Reporting Config + */ + export interface CaptureConfig { browser: { type: BrowserType; @@ -184,6 +198,11 @@ export interface JobDocPayload { type: string | null; } +export interface JobSource { + _id: string; + _source: JobDoc; +} + export interface JobDocOutput { content: string; // encoded content contentType: string; @@ -196,9 +215,11 @@ export interface JobDoc { status: string; // completed, failed, etc } -export interface JobSource { - _id: string; - _source: JobDoc; +export interface JobDocExecuted { + jobtype: string; + output: JobDocOutputExecuted; + payload: JobDocPayload; + status: string; // completed, failed, etc } /* @@ -230,7 +251,11 @@ export type ESQueueCreateJobFn = ( request: RequestFacade ) => Promise; -export type ESQueueWorkerExecuteFn = (jobId: string, job: JobDoc, cancellationToken: any) => void; +export type ESQueueWorkerExecuteFn = ( + jobId: string, + job: JobDoc, + cancellationToken?: CancellationToken +) => void; export type JobIDForImmediate = null; export type ImmediateExecuteFn = ( @@ -262,6 +287,7 @@ export interface ExportTypeDefinition { id: string; name: string; jobType: string; + jobContentEncoding?: string; jobContentExtension: string; createJobFactory: CreateJobFactory; executeJobFactory: ExecuteJobFactory | ExecuteImmediateJobFactory; From e93cfd4805dd2afb4c422bf8c1a51a33b7c331e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 14 Nov 2019 15:35:23 -0500 Subject: [PATCH 07/28] Change URLs for support menu (#50700) --- src/core/public/chrome/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/public/chrome/constants.ts b/src/core/public/chrome/constants.ts index 3411f6f629a13d..c8e53b38c618eb 100644 --- a/src/core/public/chrome/constants.ts +++ b/src/core/public/chrome/constants.ts @@ -18,6 +18,6 @@ */ export const ELASTIC_SUPPORT_LINK = 'https://support.elastic.co/'; -export const KIBANA_FEEDBACK_LINK = 'https://www.elastic.co/kibana/feedback'; -export const KIBANA_ASK_ELASTIC_LINK = 'https://www.elastic.co/kibana/ask-elastic'; +export const KIBANA_FEEDBACK_LINK = 'https://www.elastic.co/products/kibana/feedback'; +export const KIBANA_ASK_ELASTIC_LINK = 'https://www.elastic.co/products/kibana/ask-elastic'; export const GITHUB_CREATE_ISSUE_LINK = 'https://github.com/elastic/kibana/issues/new/choose'; From 51a6eeb6c2a2798a3724b412fe8e4e3c0ac4bbf2 Mon Sep 17 00:00:00 2001 From: Nate Archer Date: Thu, 14 Nov 2019 14:42:48 -0600 Subject: [PATCH 08/28] [DOCS][SIEM]: Change Kibana advanced settings to match UI (#50679) * [DOCS] Fix beta tag in Code Docs * Change kibana advanced settings to match UI * Add random line break for illustration --- docs/management/advanced-options.asciidoc | 122 +++++++++++----------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index caff7f5b1fdc6a..38fceeb47d6fd2 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -9,11 +9,12 @@ for displayed decimal values. . Scroll or search for the setting you want to modify. . Enter a new value for the setting. + [float] [[settings-read-only-access]] === [xpack]#Read only access# -When you have insufficient privileges to edit advanced settings, the following -indicator in Kibana will be displayed. The buttons to edit settings won't be visible. +When you have insufficient privileges to edit advanced settings, the following +indicator in Kibana will be displayed. The buttons to edit settings won't be visible. For more information on granting access to Kibana see <>. [role="screenshot"] @@ -25,9 +26,9 @@ image::images/settings-read-only-badge.png[Example of Advanced Settings Manageme WARNING: Modifying a setting can affect {kib} performance and cause problems that are -difficult to diagnose. Setting a property value to a blank field reverts +difficult to diagnose. Setting a property value to a blank field reverts to the default behavior, which might not be -compatible with other configuration settings. Deleting a custom setting +compatible with other configuration settings. Deleting a custom setting removes it from {kib} permanently. @@ -44,7 +45,7 @@ removes it from {kib} permanently. adapt to the interval between measurements. Keys are http://en.wikipedia.org/wiki/ISO_8601#Time_intervals[ISO8601 intervals]. `dateFormat:tz`:: The timezone that Kibana uses. The default value of `Browser` uses the timezone detected by the browser. `dateNanosFormat`:: The format to use for displaying https://momentjs.com/docs/#/displaying/format/[pretty formatted dates] of {ref}/date_nanos.html[Elasticsearch date_nanos type]. -`defaultIndex`:: The index to access if no index is set. The default is `null`. +`defaultIndex`:: The index to access if no index is set. The default is `null`. `fields:popularLimit`:: The top N most popular fields to show. `filterEditor:suggestValues`:: Set this property to `false` to prevent the filter editor from suggesting values for fields. `filters:pinnedByDefault`:: Set this property to `true` to make filters have a global state (be pinned) by default. @@ -59,46 +60,46 @@ mentioned use "\_default_". `histogram:maxBars`:: Date histograms are not generated with more bars than the value of this property, scaling values when necessary. `history:limit`:: In fields that have history, such as query inputs, show this many recent values. -`indexPattern:fieldMapping:lookBack`:: For index patterns containing timestamps in their names, +`indexPattern:fieldMapping:lookBack`:: For index patterns containing timestamps in their names, look for this many recent matching patterns from which to query the field mapping. `indexPattern:placeholder`:: The default placeholder value to use in Management > Index Patterns > Create Index Pattern. -`metaFields`:: Fields that exist outside of `_source`. Kibana merges these fields +`metaFields`:: Fields that exist outside of `_source`. Kibana merges these fields into the document when displaying it. `metrics:max_buckets`:: The maximum numbers of buckets that a single -data source can return. This might arise when the user selects a +data source can return. This might arise when the user selects a short interval (for example, 1s) for a long time period (1 year). -`query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character -in a query clause. Only applies when experimental query features are -enabled in the query bar. To disallow leading wildcards in Lucene queries, +`query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character +in a query clause. Only applies when experimental query features are +enabled in the query bar. To disallow leading wildcards in Lucene queries, use `query:queryString:options`. `query:queryString:options`:: Options for the Lucene query string parser. Only used when "Query language" is set to Lucene. -`savedObjects:listingLimit`:: The number of objects to fetch for lists of saved objects. +`savedObjects:listingLimit`:: The number of objects to fetch for lists of saved objects. The default value is 1000. Do not set above 10000. -`savedObjects:perPage`:: The number of objects to show on each page of the +`savedObjects:perPage`:: The number of objects to show on each page of the list of saved objects. The default is 5. `search:queryLanguage`:: The query language to use in the query bar. -Choices are <>, a language built specifically for {kib}, and the <>, a language built specifically for {kib}, and the <>. -`shortDots:enable`:: Set this property to `true` to shorten long +`shortDots:enable`:: Set this property to `true` to shorten long field names in visualizations. For example, show `f.b.baz` instead of `foo.bar.baz`. `sort:options`:: Options for the Elasticsearch {ref}/search-request-body.html#request-body-search-sort[sort] parameter. -`state:storeInSessionStorage`:: [experimental] Kibana tracks UI state in the -URL, which can lead to problems when there is a lot of state information, -and the URL gets very long. -Enabling this setting stores part of the URL in your browser session to keep the +`state:storeInSessionStorage`:: [experimental] Kibana tracks UI state in the +URL, which can lead to problems when there is a lot of state information, +and the URL gets very long. +Enabling this setting stores part of the URL in your browser session to keep the URL short. `theme:darkMode`:: Set to `true` to enable a dark mode for the {kib} UI. You must refresh the page to apply the setting. -`timepicker:quickRanges`:: The list of ranges to show in the Quick section of -the time filter. This should be an array of objects, with each object containing -`from`, `to` (see {ref}/common-options.html#date-math[accepted formats]), +`timepicker:quickRanges`:: The list of ranges to show in the Quick section of +the time filter. This should be an array of objects, with each object containing +`from`, `to` (see {ref}/common-options.html#date-math[accepted formats]), and `display` (the title to be displayed). `timepicker:refreshIntervalDefaults`:: The default refresh interval for the time filter. Example: `{ "display": "15 seconds", "pause": true, "value": 15000 }`. `timepicker:timeDefaults`:: The default selection in the time filter. `truncate:maxHeight`:: The maximum height that a cell occupies in a table. Set to 0 to disable truncation. -`xPack:defaultAdminEmail`:: Email address for X-Pack admin operations, such as +`xPack:defaultAdminEmail`:: Email address for X-Pack admin operations, such as cluster alert notifications from Monitoring. @@ -107,7 +108,7 @@ cluster alert notifications from Monitoring. === Accessibility settings [horizontal] -`accessibility:disableAnimations`:: Turns off all unnecessary animations in the +`accessibility:disableAnimations`:: Turns off all unnecessary animations in the {kib} UI. Refresh the page to apply the changes. [float] @@ -124,21 +125,21 @@ cluster alert notifications from Monitoring. [horizontal] `context:defaultSize`:: The number of surrounding entries to display in the context view. The default value is 5. `context:step`:: The number by which to increment or decrement the context size. The default value is 5. -`context:tieBreakerFields`:: A comma-separated list of fields to use -for breaking a tie between documents that have the same timestamp value. The first +`context:tieBreakerFields`:: A comma-separated list of fields to use +for breaking a tie between documents that have the same timestamp value. The first field that is present and sortable in the current index pattern is used. `defaultColumns`:: The columns that appear by default on the Discover page. -The default is `_source`. -`discover:aggs:terms:size`:: The number terms that are visualized when clicking +The default is `_source`. +`discover:aggs:terms:size`:: The number terms that are visualized when clicking the Visualize button in the field drop down. The default is `20`. `discover:sampleSize`:: The number of rows to show in the Discover table. `discover:sort:defaultOrder`:: The default sort direction for time-based index patterns. -`discover:searchOnPageLoad`:: Controls whether a search is executed when Discover first loads. +`discover:searchOnPageLoad`:: Controls whether a search is executed when Discover first loads. This setting does not have an effect when loading a saved search. `doc_table:hideTimeColumn`:: Hides the "Time" column in Discover and in all saved searches on dashboards. -`doc_table:highlight`:: Highlights results in Discover and saved searches on dashboards. +`doc_table:highlight`:: Highlights results in Discover and saved searches on dashboards. Highlighting slows requests when -working on big documents. +working on big documents. @@ -150,14 +151,14 @@ working on big documents. [horizontal] `notifications:banner`:: A custom banner intended for temporary notices to all users. Supports https://help.github.com/en/articles/basic-writing-and-formatting-syntax[Markdown]. -`notifications:lifetime:banner`:: The duration, in milliseconds, for banner -notification displays. The default value is 3000000. Set this field to `Infinity` +`notifications:lifetime:banner`:: The duration, in milliseconds, for banner +notification displays. The default value is 3000000. Set this field to `Infinity` to disable banner notifications. -`notifications:lifetime:error`:: The duration, in milliseconds, for error +`notifications:lifetime:error`:: The duration, in milliseconds, for error notification displays. The default value is 300000. Set this field to `Infinity` to disable error notifications. -`notifications:lifetime:info`:: The duration, in milliseconds, for information notification displays. +`notifications:lifetime:info`:: The duration, in milliseconds, for information notification displays. The default value is 5000. Set this field to `Infinity` to disable information notifications. -`notifications:lifetime:warning`:: The duration, in milliseconds, for warning notification +`notifications:lifetime:warning`:: The duration, in milliseconds, for warning notification displays. The default value is 10000. Set this field to `Infinity` to disable warning notifications. @@ -175,8 +176,8 @@ displays. The default value is 10000. Set this field to `Infinity` to disable wa === Rollup settings [horizontal] -`rollups:enableIndexPatterns`:: Enables the creation of index patterns that -capture rollup indices, which in turn enables visualizations based on rollup data. +`rollups:enableIndexPatterns`:: Enables the creation of index patterns that +capture rollup indices, which in turn enables visualizations based on rollup data. Refresh the page to apply the changes. @@ -188,22 +189,22 @@ Refresh the page to apply the changes. `courier:batchSearches`:: When disabled, dashboard panels will load individually, and search requests will terminate when users navigate away or update the query. When enabled, dashboard panels will load together when all of the data is loaded, and searches will not terminate. -`courier:customRequestPreference`:: {ref}/search-request-body.html#request-body-search-preference[Request preference] +`courier:customRequestPreference`:: {ref}/search-request-body.html#request-body-search-preference[Request preference] to use when `courier:setRequestPreference` is set to "custom". -`courier:ignoreFilterIfFieldNotInIndex`:: Skips filters that apply to fields that don't exist in the index for a visualization. +`courier:ignoreFilterIfFieldNotInIndex`:: Skips filters that apply to fields that don't exist in the index for a visualization. Useful when dashboards consist of visualizations from multiple index patterns. -`courier:maxConcurrentShardRequests`:: Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] -setting used for `_msearch` requests sent by {kib}. Set to 0 to disable this +`courier:maxConcurrentShardRequests`:: Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] +setting used for `_msearch` requests sent by {kib}. Set to 0 to disable this config and use the {es} default. `courier:setRequestPreference`:: Enables you to set which shards handle your search requests. -* *Session ID:* Restricts operations to execute all search requests on the same shards. +* *Session ID:* Restricts operations to execute all search requests on the same shards. This has the benefit of reusing shard caches across requests. -* *Custom:* Allows you to define your own preference. Use `courier:customRequestPreference` +* *Custom:* Allows you to define your own preference. Use `courier:customRequestPreference` to customize your preference value. -* *None:* Do not set a preference. This might provide better performance -because requests can be spread across all shard copies. However, results might +* *None:* Do not set a preference. This might provide better performance +because requests can be spread across all shard copies. However, results might be inconsistent because different shards might be in different refresh states. -`search:includeFrozen`:: Includes {ref}/frozen-indices.html[frozen indices] in results. +`search:includeFrozen`:: Includes {ref}/frozen-indices.html[frozen indices] in results. Searching through frozen indices might increase the search time. This setting is off by default. Users must opt-in to include frozen indices. @@ -212,8 +213,8 @@ might increase the search time. This setting is off by default. Users must opt-i === SIEM settings [horizontal] -`siem:defaultAnomalyScore`:: The threshold above which anomalies are displayed in the SIEM app. -`siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. +`siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app. +`siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. `siem:refreshIntervalDefaults`:: The default refresh interval for the SIEM time filter, in milliseconds. `siem:timeDefaults`:: The default period of time in the SIEM time filter. @@ -226,16 +227,16 @@ might increase the search time. This setting is off by default. Users must opt-i `timelion:default_rows`:: The default number of rows to use on a Timelion sheet. `timelion:es.default_index`:: The default index when using the `.es()` query. `timelion:es.timefield`:: The default field containing a timestamp when using the `.es()` query. -`timelion:graphite.url`:: [experimental] Used with graphite queries, this is the URL of your graphite host -in the form https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite. This URL can be +`timelion:graphite.url`:: [experimental] Used with graphite queries, this is the URL of your graphite host +in the form https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite. This URL can be selected from a whitelist configured in the `kibana.yml` under `timelion.graphiteUrls`. `timelion:max_buckets`:: The maximum number of buckets a single data source can return. This value is used for calculating automatic intervals in visualizations. `timelion:min_interval`:: The smallest interval to calculate when using "auto". `timelion:quandl.key`:: [experimental] Used with quandl queries, this is your API key from https://www.quandl.com/[www.quandl.com]. -`timelion:showTutorial`:: Shows the Timelion tutorial +`timelion:showTutorial`:: Shows the Timelion tutorial to users when they first open the Timelion app. -`timelion:target_buckets`:: Used for calculating automatic intervals in visualizations, +`timelion:target_buckets`:: Used for calculating automatic intervals in visualizations, this is the number of buckets to try to represent. @@ -246,18 +247,18 @@ this is the number of buckets to try to represent. [horizontal] `visualization:colorMapping`:: Maps values to specified colors in visualizations. -`visualization:dimmingOpacity`:: The opacity of the chart items that are dimmed -when highlighting another element of the chart. The lower this number, the more +`visualization:dimmingOpacity`:: The opacity of the chart items that are dimmed +when highlighting another element of the chart. The lower this number, the more the highlighted element stands out. This must be a number between 0 and 1. -`visualization:loadingDelay`:: The time to wait before dimming visualizations +`visualization:loadingDelay`:: The time to wait before dimming visualizations during a query. -`visualization:regionmap:showWarnings`:: Shows +`visualization:regionmap:showWarnings`:: Shows a warning in a region map when terms cannot be joined to a shape. `visualization:tileMap:WMSdefaults`:: The default properties for the WMS map server support in the coordinate map. `visualization:tileMap:maxPrecision`:: The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, -and 12 is the maximum. See this +and 12 is the maximum. See this {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[explanation of cell dimensions]. -`visualize:enableLabs`:: Enables users to create, view, and edit experimental visualizations. +`visualize:enableLabs`:: Enables users to create, view, and edit experimental visualizations. If disabled, only visualizations that are considered production-ready are available to the user. @@ -265,6 +266,5 @@ If disabled, only visualizations that are considered production-ready are availa [[kibana-telemetry-settings]] === Usage data settings -Helps improve the Elastic Stack by providing usage statistics for +Helps improve the Elastic Stack by providing usage statistics for basic features. This data will not be shared outside of Elastic. - From c40b32010462aa6731d5c8a9312444228b3a819e Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 14 Nov 2019 16:50:23 -0600 Subject: [PATCH 09/28] [Logs UI] Add IE11-specific CSS fixes for anomalies table (#49980) * [Logs UI] Add IE11-specific CSS fixes for anomalies table * Switch to table-layout fix --- .../pages/logs/analysis/sections/anomalies/table.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx index ebf31d8320df55..76eb2fae770598 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx @@ -12,6 +12,7 @@ import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; import { LogRateResults } from '../../../../../containers/logs/log_analysis/log_analysis_results'; import { AnomaliesTableExpandedRow } from './expanded_row'; import { formatAnomalyScore, getFriendlyNameForPartitionId } from '../helpers/data_formatters'; +import euiStyled from '../../../../../../../../common/eui_styled_components'; interface TableItem { id: string; @@ -154,7 +155,7 @@ export const AnomaliesTable: React.FunctionComponent<{ ]; return ( - ); }; + +const StyledEuiBasicTable = euiStyled(EuiBasicTable)` + & .euiTable { + table-layout: auto; + } +`; From bd96d738dc36599d1b4a6ea69a9dbfac442f5442 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Thu, 14 Nov 2019 15:28:32 -0800 Subject: [PATCH 10/28] [DOCS] Adds documentation on telemetry settings (#50739) * [DOCS] Adds documentation on telemetry settings * [DOCS] Adds not that both settings can't be false at the same time --- docs/setup/settings.asciidoc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 11a50fea92f052..f4434ea7a09f46 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -320,6 +320,18 @@ supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2 setting this to `true` enables unauthenticated users to access the Kibana server status API and status page. +`telemetry.allowChangingOptInStatus`:: *Default: true*. If `true`, +users are able to change the telemetry setting at a later time in +<>. If `false`, +{kib} looks at the value of `telemetry.optIn` to determine whether to send +telemetry data or not. `telemetry.allowChangingOptInStatus` and `telemetry.optIn` +cannot be `false` at the same time. + +`telemetry.optIn`:: *Default: true* If `true`, telemetry data is sent to Elastic. + If `false`, collection of telemetry data is disabled. + To enable telemetry and prevent users from disabling it, + set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. + `vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch. `xpack.license_management.enabled`:: *Default: true* Set this value to false to From 48a2156c4cd03b8bd523d5aff2bada12bb4e1ad1 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 15 Nov 2019 00:19:57 +0000 Subject: [PATCH 11/28] chore(NA): remove code plugin from codeowners (#50451) --- .github/CODEOWNERS | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 94296d076189bf..0f1136fd5334ba 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -28,11 +28,6 @@ # Canvas /x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas -# Code -/x-pack/legacy/plugins/code/ @teams/code -/x-pack/test/functional/apps/code/ @teams/code -/x-pack/test/api_integration/apis/code/ @teams/code - # Logs & Metrics UI /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui /x-pack/legacy/plugins/integrations_manager/ @elastic/epm From 3131dd494d623c72690b4a6335f109d19e6d3cc3 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 14 Nov 2019 20:45:21 -0500 Subject: [PATCH 12/28] [SIEM] Add SavedQuery in Timeline (#49813) * Step-1: Add Search Bar in timeline instead of our own kql * Step-2: Add the saved query with filter in timeline savedObject * fix type * Fix unit test * fix bug when you use an exists filter * Fix bug to do a search when add filter by itself * Review I * unit tests * fix import for Filter * add range as a filter * remove comment * forget to add range in ES mapping + allow query with only filters * fix and/or with filter * review with Liza --- .../components/query_bar_top_row.tsx | 7 +- .../search_bar/components/search_bar.tsx | 20 +- .../common/es_query/filters/meta_filter.ts | 10 +- .../legacy/plugins/siem/common/constants.ts | 1 + .../legacy/plugins/siem/public/apps/index.ts | 5 +- .../plugins/siem/public/apps/plugin.tsx | 8 +- .../plugins/siem/public/apps/start_app.tsx | 32 +- .../public/components/flyout/index.test.tsx | 2 + .../components/flyout/pane/index.test.tsx | 9 +- .../components/open_timeline/helpers.test.ts | 161 +++++++ .../components/open_timeline/helpers.ts | 34 ++ .../page/hosts/hosts_table/index.test.tsx | 5 +- .../components/query_bar/index.test.tsx | 342 +++++++++++++++ .../public/components/query_bar/index.tsx | 146 +++++++ .../public/components/search_bar/index.tsx | 6 +- .../super_date_picker/index.test.tsx | 88 +--- .../components/super_date_picker/index.tsx | 16 +- .../timeline/fetch_kql_timeline.tsx | 1 - .../header/__snapshots__/index.test.tsx.snap | 1 + .../components/timeline/header/index.test.tsx | 9 + .../components/timeline/header/index.tsx | 6 +- .../components/timeline/helpers.test.tsx | 48 +++ .../public/components/timeline/helpers.tsx | 9 +- .../siem/public/components/timeline/index.tsx | 10 + .../timeline/properties/index.test.tsx | 8 + .../timeline/query_bar/index.test.tsx | 406 ++++++++++++++++++ .../components/timeline/query_bar/index.tsx | 310 +++++++++++++ .../timeline/search_or_filter/index.tsx | 190 ++++++-- .../search_or_filter/search_or_filter.tsx | 93 ++-- .../components/timeline/timeline.test.tsx | 13 + .../public/components/timeline/timeline.tsx | 5 +- .../timeline/one/index.gql_query.ts | 22 + .../containers/timeline/persist.gql_query.ts | 22 + .../siem/public/graphql/introspection.json | 376 +++++++++++++++- .../plugins/siem/public/graphql/types.ts | 196 ++++++++- .../plugins/siem/public/lib/keury/index.ts | 16 +- .../plugins/siem/public/mock/kibana_config.ts | 56 +++ .../siem/public/mock/test_providers.tsx | 60 ++- .../plugins/siem/public/mock/ui_settings.ts | 6 + .../pages/hosts/details/details_tabs.test.tsx | 5 +- .../siem/public/pages/hosts/hosts.test.tsx | 5 +- .../pages/network/ip_details/index.test.tsx | 5 +- .../public/pages/network/network.test.tsx | 5 +- .../siem/public/store/inputs/selectors.ts | 3 + .../legacy/plugins/siem/public/store/model.ts | 4 +- .../siem/public/store/timeline/actions.ts | 11 + .../siem/public/store/timeline/epic.test.ts | 283 ++++++++++++ .../siem/public/store/timeline/epic.ts | 89 +++- .../siem/public/store/timeline/helpers.ts | 42 ++ .../siem/public/store/timeline/model.ts | 5 + .../siem/public/store/timeline/reducer.ts | 26 +- .../public/utils/kql/use_update_kql.test.tsx | 7 - .../siem/public/utils/kql/use_update_kql.tsx | 8 +- .../utils/saved_query_services/index.tsx | 25 ++ .../server/graphql/timeline/schema.gql.ts | 54 ++- .../plugins/siem/server/graphql/types.ts | 284 +++++++++++- .../lib/timeline/saved_object_mappings.ts | 62 +++ .../plugins/siem/server/lib/timeline/types.ts | 29 ++ 58 files changed, 3450 insertions(+), 257 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx index ca0ac3c3718497..cd64b1ecf25492 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_top_row.tsx @@ -82,8 +82,11 @@ function QueryBarTopRowUI(props: Props) { const queryLanguage = props.query && props.query.language; const persistedLog: PersistedLog | undefined = React.useMemo( - () => (queryLanguage ? getQueryLog(uiSettings!, storage, appName, queryLanguage) : undefined), - [queryLanguage] + () => + queryLanguage && uiSettings && storage && appName + ? getQueryLog(uiSettings!, storage, appName, queryLanguage) + : undefined, + [appName, queryLanguage, uiSettings, storage] ); function onClickSubmitButton(event: React.MouseEvent) { diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index ea0f6775e4831d..d713139366eef4 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -72,6 +72,7 @@ export interface SearchBarOwnProps { // Show when user has privileges to save showSaveQuery?: boolean; savedQuery?: SavedQuery; + onQueryChange?: (payload: { dateRange: TimeRange; query?: Query }) => void; onQuerySubmit?: (payload: { dateRange: TimeRange; query?: Query }) => void; // User has saved the current state as a saved query onSaved?: (savedQuery: SavedQuery) => void; @@ -206,6 +207,18 @@ class SearchBarUI extends Component { ); } + /* + * This Function is here to show the toggle in saved query form + * in case you the date range (from/to) + */ + private shouldRenderTimeFilterInSavedQueryForm() { + const { dateRangeFrom, dateRangeTo, showDatePicker } = this.props; + return ( + showDatePicker || + (!showDatePicker && dateRangeFrom !== undefined && dateRangeTo !== undefined) + ); + } + public setFilterBarHeight = () => { requestAnimationFrame(() => { const height = @@ -299,6 +312,9 @@ class SearchBarUI extends Component { dateRangeFrom: queryAndDateRange.dateRange.from, dateRangeTo: queryAndDateRange.dateRange.to, }); + if (this.props.onQueryChange) { + this.props.onQueryChange(queryAndDateRange); + } }; public onQueryBarSubmit = (queryAndDateRange: { dateRange?: TimeRange; query?: Query }) => { @@ -440,7 +456,7 @@ class SearchBarUI extends Component { onSave={this.onSave} onClose={() => this.setState({ showSaveQueryModal: false })} showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.props.showDatePicker} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} /> ) : null} {this.state.showSaveNewQueryModal ? ( @@ -449,7 +465,7 @@ class SearchBarUI extends Component { onSave={savedQueryMeta => this.onSave(savedQueryMeta, true)} onClose={() => this.setState({ showSaveNewQueryModal: false })} showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.props.showDatePicker} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} /> ) : null} diff --git a/src/plugins/data/common/es_query/filters/meta_filter.ts b/src/plugins/data/common/es_query/filters/meta_filter.ts index 9adfdc4eedcb33..ff6dff9d8b7490 100644 --- a/src/plugins/data/common/es_query/filters/meta_filter.ts +++ b/src/plugins/data/common/es_query/filters/meta_filter.ts @@ -33,15 +33,17 @@ export interface FilterValueFormatter { } export interface FilterMeta { + alias: string | null; + disabled: boolean; + negate: boolean; + // controlledBy is there to identify who owns the filter + controlledBy?: string; // index and type are optional only because when you create a new filter, there are no defaults index?: string; type?: string; - disabled: boolean; - negate: boolean; - alias: string | null; key?: string; - value?: string | ((formatter?: FilterValueFormatter) => string); params?: any; + value?: string | ((formatter?: FilterValueFormatter) => string); } export interface Filter { diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 6845648ee921dd..2b4b4b78db6269 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -25,6 +25,7 @@ export const DEFAULT_TO = 'now'; export const DEFAULT_INTERVAL_PAUSE = true; export const DEFAULT_INTERVAL_TYPE = 'manual'; export const DEFAULT_INTERVAL_VALUE = 300000; // ms +export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; /** * Id for the SIGNALS alerting type diff --git a/x-pack/legacy/plugins/siem/public/apps/index.ts b/x-pack/legacy/plugins/siem/public/apps/index.ts index 468e72c8a2e5ca..b71c4fe699860e 100644 --- a/x-pack/legacy/plugins/siem/public/apps/index.ts +++ b/x-pack/legacy/plugins/siem/public/apps/index.ts @@ -9,4 +9,7 @@ import { npStart } from 'ui/new_platform'; import { Plugin } from './plugin'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -new Plugin({ opaqueId: Symbol('siem'), env: {} as any }, chrome).start(npStart); +new Plugin({ opaqueId: Symbol('siem'), env: {} as any }, chrome).start( + npStart.core, + npStart.plugins +); diff --git a/x-pack/legacy/plugins/siem/public/apps/plugin.tsx b/x-pack/legacy/plugins/siem/public/apps/plugin.tsx index f3cbd44f34cdc2..1f19841788ddb6 100644 --- a/x-pack/legacy/plugins/siem/public/apps/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/apps/plugin.tsx @@ -15,11 +15,6 @@ import template from './template.html'; export const ROOT_ELEMENT_ID = 'react-siem-root'; -export interface StartObject { - core: LegacyCoreStart; - plugins: PluginsStart; -} - export class Plugin { constructor( // @ts-ignore this is added to satisfy the New Platform typing constraint, @@ -30,8 +25,7 @@ export class Plugin { this.chrome = chrome; } - public start(start: StartObject): void { - const { core, plugins } = start; + public start(core: LegacyCoreStart, plugins: PluginsStart) { // @ts-ignore improper type description this.chrome.setRootTemplate(template); const checkForRoot = () => { diff --git a/x-pack/legacy/plugins/siem/public/apps/start_app.tsx b/x-pack/legacy/plugins/siem/public/apps/start_app.tsx index 47c8b6eee6452c..4549db946b815d 100644 --- a/x-pack/legacy/plugins/siem/public/apps/start_app.tsx +++ b/x-pack/legacy/plugins/siem/public/apps/start_app.tsx @@ -9,6 +9,8 @@ import React, { memo, FC } from 'react'; import { ApolloProvider } from 'react-apollo'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { ThemeProvider } from 'styled-components'; +import { LegacyCoreStart } from 'kibana/public'; +import { PluginsStart } from 'ui/new_platform/new_platform'; import { EuiErrorBoundary } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; @@ -17,6 +19,9 @@ import { BehaviorSubject } from 'rxjs'; import { pluck } from 'rxjs/operators'; import { I18nContext } from 'ui/i18n'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; + import { DEFAULT_DARK_MODE } from '../../common/constants'; import { ErrorToastDispatcher } from '../components/error_toast_dispatcher'; import { compose } from '../lib/compose/kibana_compose'; @@ -31,8 +36,6 @@ import { MlCapabilitiesProvider } from '../components/ml/permissions/ml_capabili import { ApolloClientContext } from '../utils/apollo_context'; -import { StartObject } from './plugin'; - const StartApp: FC = memo(libs => { const history = createHashHistory(); @@ -74,10 +77,21 @@ const StartApp: FC = memo(libs => { export const ROOT_ELEMENT_ID = 'react-siem-root'; -export const SiemApp = memo(({ core, plugins }) => ( - - - - - -)); +export const SiemApp = memo<{ core: LegacyCoreStart; plugins: PluginsStart }>( + ({ core, plugins }) => ( + + + + + + + + ) +); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx index a7603762f424e5..ddc3e4f15938ae 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx @@ -20,6 +20,8 @@ import { FlyoutButton } from './button'; const testFlyoutHeight = 980; const usersViewing = ['elastic']; +jest.mock('../../lib/settings/use_kibana_ui_setting'); + describe('Flyout', () => { const state: State = mockGlobalState; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx index 6f0f7a45e5c4ed..6681e5a90b1a4a 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -10,14 +10,21 @@ import 'jest-styled-components'; import * as React from 'react'; import { flyoutHeaderHeight } from '../'; +import { useKibanaCore } from '../../../lib/compose/kibana_core'; import { TestProviders } from '../../../mock'; - +import { mockUiSettings } from '../../../mock/ui_settings'; import { Pane } from '.'; const testFlyoutHeight = 980; const testWidth = 640; const usersViewing = ['elastic']; +const mockUseKibanaCore = useKibanaCore as jest.Mock; +jest.mock('../../../lib/compose/kibana_core'); +mockUseKibanaCore.mockImplementation(() => ({ + uiSettings: mockUiSettings, +})); + describe('Pane', () => { test('renders correctly against snapshot', () => { const EmptyComponent = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts index fbd3f300496474..840d8c0a481241 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts @@ -235,6 +235,7 @@ describe('helpers', () => { }, description: '', eventIdToNoteIds: {}, + filters: [], highlightedDropAndProviderId: '', historyIds: [], id: 'savedObject-1', @@ -321,6 +322,7 @@ describe('helpers', () => { }, description: '', eventIdToNoteIds: {}, + filters: [], highlightedDropAndProviderId: '', historyIds: [], id: 'savedObject-1', @@ -400,6 +402,165 @@ describe('helpers', () => { dataProviders: [], description: '', eventIdToNoteIds: {}, + filters: [], + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + title: '', + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + dateRange: { + start: 0, + end: 0, + }, + show: false, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + width: 1100, + id: 'savedObject-1', + }); + }); + + test('should merge filters object back with json object', () => { + const timeline = { + savedObjectId: 'savedObject-1', + columns: timelineDefaults.columns.filter(column => column.id !== 'event.action'), + filters: [ + { + meta: { + alias: null, + controlledBy: null, + disabled: false, + index: null, + key: 'event.category', + negate: false, + params: '{"query":"file"}', + type: 'phrase', + value: null, + }, + query: '{"match_phrase":{"event.category":"file"}}', + exists: null, + }, + { + meta: { + alias: null, + controlledBy: null, + disabled: false, + index: null, + key: '@timestamp', + negate: false, + params: null, + type: 'exists', + value: 'exists', + }, + query: null, + exists: '{"field":"@timestamp"}', + }, + ], + version: '1', + }; + + const newTimeline = defaultTimelineToTimelineModel(timeline, false); + expect(newTimeline).toEqual({ + savedObjectId: 'savedObject-1', + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + version: '1', + dataProviders: [], + description: '', + eventIdToNoteIds: {}, + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + controlledBy: null, + disabled: false, + index: null, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + value: null, + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: 'appState', + }, + exists: { + field: '@timestamp', + }, + meta: { + alias: null, + controlledBy: null, + disabled: false, + index: null, + key: '@timestamp', + negate: false, + params: null, + type: 'exists', + value: 'exists', + }, + }, + ], highlightedDropAndProviderId: '', historyIds: [], isFavorite: false, diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts index 1d2508fcfaf155..91480f20d8b004 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts @@ -61,6 +61,14 @@ const omitTypename = (key: string, value: keyof TimelineModel) => const omitTypenameInTimeline = (timeline: TimelineResult): TimelineResult => JSON.parse(JSON.stringify(timeline), omitTypename); +const parseString = (params: string) => { + try { + return JSON.parse(params); + } catch { + return params; + } +}; + export const defaultTimelineToTimelineModel = ( timeline: TimelineResult, duplicate: boolean @@ -97,6 +105,32 @@ export const defaultTimelineToTimelineModel = ( return acc; }, {}) : {}, + filters: + timeline.filters != null + ? timeline.filters.map(filter => ({ + $state: { + store: 'appState', + }, + meta: { + ...filter.meta, + ...(filter.meta && filter.meta.field != null + ? { params: parseString(filter.meta.field) } + : {}), + ...(filter.meta && filter.meta.params != null + ? { params: parseString(filter.meta.params) } + : {}), + ...(filter.meta && filter.meta.value != null + ? { value: parseString(filter.meta.value) } + : {}), + }, + ...(filter.exists != null ? { exists: parseString(filter.exists) } : {}), + ...(filter.match_all != null ? { exists: parseString(filter.match_all) } : {}), + ...(filter.missing != null ? { exists: parseString(filter.missing) } : {}), + ...(filter.query != null ? { query: parseString(filter.query) } : {}), + ...(filter.range != null ? { range: parseString(filter.range) } : {}), + ...(filter.script != null ? { exists: parseString(filter.script) } : {}), + })) + : [], isFavorite: duplicate ? false : timeline.favorite != null diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx index 44898e7a307ff2..cdeaba0f067f5d 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx @@ -30,10 +30,13 @@ mockUseKibanaCore.mockImplementation(() => ({ })); // Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar +// For now let's forget about SiemSearchBar and QueryBar jest.mock('../../../search_bar', () => ({ SiemSearchBar: () => null, })); +jest.mock('../../../query_bar', () => ({ + QueryBar: () => null, +})); describe('Hosts Table', () => { const loadPage = jest.fn(); diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx new file mode 100644 index 00000000000000..d619b515ccc7a2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx @@ -0,0 +1,342 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { SearchBar } from '../../../../../../../src/legacy/core_plugins/data/public'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { uiSettingsServiceMock } from '../../../../../../../src/core/public/ui_settings/ui_settings_service.mock'; +import { useKibanaCore } from '../../lib/compose/kibana_core'; +import { TestProviders, mockIndexPattern } from '../../mock'; +import { QueryBar, QueryBarComponentProps } from '.'; +import { DEFAULT_FROM, DEFAULT_TO } from '../../../common/constants'; +import { mockUiSettings } from '../../mock/ui_settings'; + +jest.mock('ui/new_platform'); + +const mockUseKibanaCore = useKibanaCore as jest.Mock; +const mockUiSettingsForFilterManager = uiSettingsServiceMock.createSetupContract(); +jest.mock('../../lib/compose/kibana_core'); +mockUseKibanaCore.mockImplementation(() => ({ + uiSettings: mockUiSettings, + savedObjects: {}, +})); + +describe('QueryBar ', () => { + // We are doing that because we need to wrapped this component with redux + // and redux does not like to be updated and since we need to update our + // child component (BODY) and we do not want to scare anyone with this error + // we are hiding it!!! + // eslint-disable-next-line no-console + const originalError = console.error; + beforeAll(() => { + // eslint-disable-next-line no-console + console.error = (...args: string[]) => { + if (/ does not support changing `store` on the fly/.test(args[0])) { + return; + } + originalError.call(console, ...args); + }; + }); + + const mockOnChangeQuery = jest.fn(); + const mockOnSubmitQuery = jest.fn(); + const mockOnSavedQuery = jest.fn(); + + beforeEach(() => { + mockOnChangeQuery.mockClear(); + mockOnSubmitQuery.mockClear(); + mockOnSavedQuery.mockClear(); + }); + + test('check if we format the appropriate props to QueryBar', () => { + const wrapper = mount( + + + + ); + const { + customSubmitButton, + timeHistory, + onClearSavedQuery, + onFiltersUpdated, + onQueryChange, + onQuerySubmit, + onSaved, + onSavedQueryUpdated, + ...searchBarProps + } = wrapper.find(SearchBar).props(); + + expect(searchBarProps).toEqual({ + dateRangeFrom: 'now-24h', + dateRangeTo: 'now', + filters: [], + indexPatterns: [ + { + fields: [ + { + aggregatable: true, + name: '@timestamp', + searchable: true, + type: 'date', + }, + { + aggregatable: true, + name: '@version', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.id', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test1', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test2', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test3', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test4', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test5', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test6', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test7', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test8', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'host.name', + searchable: true, + type: 'string', + }, + ], + title: 'filebeat-*,auditbeat-*,packetbeat-*', + }, + ], + isRefreshPaused: true, + query: { + language: 'kuery', + query: 'here: query', + }, + refreshInterval: undefined, + showAutoRefreshOnly: false, + showDatePicker: false, + showFilterBar: true, + showQueryBar: true, + showQueryInput: true, + showSaveQuery: true, + }); + }); + + describe('#onQueryChange', () => { + test(' is the only reference that changed when filterQueryDraft props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]'); + queryInput.simulate('change', { target: { value: 'hello: world' } }); + wrapper.update(); + + expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + }); + + describe('#onQuerySubmit', () => { + test(' is the only reference that changed when filterQuery props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + + test(' is only reference that changed when timelineId props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ onSubmitQuery: jest.fn() }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + }); + + describe('#onSavedQueryUpdated', () => { + test('is only reference that changed when dataProviders props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ onSavedQuery: jest.fn() }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx new file mode 100644 index 00000000000000..c7e58532fc7e5b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual } from 'lodash/fp'; +import React, { memo, useState, useEffect, useMemo, useCallback } from 'react'; +import { StaticIndexPattern, IndexPattern } from 'ui/index_patterns'; + +import { SavedQuery, SearchBar } from '../../../../../../../src/legacy/core_plugins/data/public'; +import { + esFilters, + FilterManager, + Query, + TimeHistory, + TimeRange, +} from '../../../../../../../src/plugins/data/public'; +import { SavedQueryTimeFilter } from '../../../../../../../src/legacy/core_plugins/data/public/search'; +import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; + +export interface QueryBarComponentProps { + dateRangeFrom?: string; + dateRangeTo?: string; + hideSavedQuery?: boolean; + indexPattern: StaticIndexPattern; + isRefreshPaused?: boolean; + filterQuery: Query; + filterManager: FilterManager; + filters: esFilters.Filter[]; + onChangedQuery: (query: Query) => void; + onSubmitQuery: (query: Query, timefilter?: SavedQueryTimeFilter) => void; + refreshInterval?: number; + savedQuery?: SavedQuery | null; + onSavedQuery: (savedQuery: SavedQuery | null) => void; +} + +export const QueryBar = memo( + ({ + dateRangeFrom, + dateRangeTo, + hideSavedQuery = false, + indexPattern, + isRefreshPaused, + filterQuery, + filterManager, + filters, + onChangedQuery, + onSubmitQuery, + refreshInterval, + savedQuery, + onSavedQuery, + }) => { + const [draftQuery, setDraftQuery] = useState(filterQuery); + + useEffect(() => { + setDraftQuery(filterQuery); + }, [filterQuery]); + + const onQuerySubmit = useCallback( + (payload: { dateRange: TimeRange; query?: Query }) => { + if (payload.query != null && !isEqual(payload.query, filterQuery)) { + onSubmitQuery(payload.query); + } + }, + [filterQuery, onSubmitQuery] + ); + + const onQueryChange = useCallback( + (payload: { dateRange: TimeRange; query?: Query }) => { + if (payload.query != null && !isEqual(payload.query, draftQuery)) { + setDraftQuery(payload.query); + onChangedQuery(payload.query); + } + }, + [draftQuery, onChangedQuery, setDraftQuery] + ); + + const onSaved = useCallback( + (newSavedQuery: SavedQuery) => { + onSavedQuery(newSavedQuery); + }, + [onSavedQuery] + ); + + const onSavedQueryUpdated = useCallback( + (savedQueryUpdated: SavedQuery) => { + const { query: newQuery, filters: newFilters, timefilter } = savedQueryUpdated.attributes; + onSubmitQuery(newQuery, timefilter); + filterManager.setFilters(newFilters || []); + onSavedQuery(savedQueryUpdated); + }, + [filterManager, onSubmitQuery, onSavedQuery] + ); + + const onClearSavedQuery = useCallback(() => { + if (savedQuery != null) { + onSubmitQuery({ + query: '', + language: savedQuery.attributes.query.language, + }); + filterManager.setFilters([]); + onSavedQuery(null); + } + }, [filterManager, onSubmitQuery, onSavedQuery, savedQuery]); + + const onFiltersUpdated = useCallback( + (newFilters: esFilters.Filter[]) => { + filterManager.setFilters(newFilters); + }, + [filterManager] + ); + + const CustomButton = <>{null}; + const indexPatterns = useMemo(() => [indexPattern as IndexPattern], [indexPattern]); + + const searchBarProps = savedQuery != null ? { savedQuery } : {}; + + return ( + + ); + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx index f5a99c631131f3..850d78e1c3428c 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx @@ -234,7 +234,7 @@ const SearchBarComponent = memo { let isSubscribed = true; @@ -258,13 +258,13 @@ const SearchBarComponent = memo [indexPattern as IndexPattern], [indexPattern]); + const indexPatterns = useMemo(() => [indexPattern as IndexPattern], [indexPattern]); return ( { describe('#SuperDatePicker', () => { const state: State = mockGlobalState; @@ -74,38 +76,6 @@ describe('SIEM Super Date Picker', () => { expect(store.getState().inputs.global.timerange.toStr).toBe('now/d'); }); - test('Make Sure it is this week', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_This_week"]') - .first() - .simulate('click'); - wrapper.update(); - expect(store.getState().inputs.global.timerange.fromStr).toBe('now/w'); - expect(store.getState().inputs.global.timerange.toStr).toBe('now/w'); - }); - - test('Make Sure it is week to date', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_Week_to date"]') - .first() - .simulate('click'); - wrapper.update(); - expect(store.getState().inputs.global.timerange.fromStr).toBe('now/w'); - expect(store.getState().inputs.global.timerange.toStr).toBe('now'); - }); - test('Make Sure to (end date) is superior than from (start date)', () => { expect(store.getState().inputs.global.timerange.to).toBeGreaterThan( store.getState().inputs.global.timerange.from @@ -168,60 +138,6 @@ describe('SIEM Super Date Picker', () => { ).toBe('Last 15 minutesToday'); }); - test('Today and Year to date is in Recently used date ranges', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_Year_to date"]') - .first() - .simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('div.euiQuickSelectPopover__section') - .at(1) - .text() - ).toBe('Year to dateToday'); - }); - - test('Today and Last 15 minutes and Year to date is in Recently used date ranges', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('button.euiQuickSelect__applyButton') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_Year_to date"]') - .first() - .simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('div.euiQuickSelectPopover__section') - .at(1) - .text() - ).toBe('Year to dateLast 15 minutesToday'); - }); - test('Make sure that it does not add any duplicate if you click again on today', () => { wrapper .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx index caeb29fc6de7d6..a2e190da0f7bc0 100644 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx @@ -12,11 +12,13 @@ import { OnRefreshProps, OnTimeChangeProps, } from '@elastic/eui'; -import { getOr, take } from 'lodash/fp'; +import { getOr, take, isEmpty } from 'lodash/fp'; import React, { useState, useCallback } from 'react'; import { connect } from 'react-redux'; - import { Dispatch } from 'redux'; + +import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../common/constants'; +import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting'; import { inputsModel, State } from '../../store'; import { inputsActions, timelineActions } from '../../store/actions'; import { InputsModelId } from '../../store/inputs/constants'; @@ -194,8 +196,18 @@ export const SuperDatePickerComponent = React.memo( const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); + const [quickRanges] = useKibanaUiSetting(DEFAULT_TIMEPICKER_QUICK_RANGES); + const commonlyUsedRanges = isEmpty(quickRanges) + ? [] + : quickRanges.map(({ from, to, display }: { from: string; to: string; display: string }) => ({ + start: from, + end: to, + label: display, + })); + return ( ( kueryFilterQuery, kueryFilterQueryDraft, storeType: 'timelineType', - type: null, timelineId: id, }), }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap index a2d20ff0b8d186..048ca080772f62 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -150,6 +150,7 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` show={true} /> ({ + uiSettings: mockUiSettings, + savedObjects: {}, +})); + describe('Header', () => { const indexPattern = mockIndexPattern; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx index f7802203d12581..9377668b4fdaf8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx @@ -84,7 +84,11 @@ export const TimelineHeader = React.memo( onToggleDataProviderExcluded={onToggleDataProviderExcluded} show={show} /> - + ) ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx index b30771760bad37..7664814f71147d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx @@ -6,6 +6,8 @@ import { cloneDeep } from 'lodash/fp'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; +import { FilterStateStore } from '../../../../../../../src/plugins/data/common/es_query/filters'; import { mockIndexPattern } from '../../mock'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; @@ -160,6 +162,52 @@ describe('Combined Queries', () => { }); }); + test('No Data Provider & No kqlQuery & with Filters', () => { + const isEventViewer = true; + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { query: 'file' }, + type: 'phrase', + }, + query: { match_phrase: { 'event.category': 'file' } }, + }, + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'host.name', + negate: false, + type: 'exists', + value: 'exists', + }, + exists: { field: 'host.name' }, + } as esFilters.Filter, + ], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + isEventViewer, + }) + ).toEqual({ + filterQuery: + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', + }); + }); + test('Only Data Provider', () => { const dataProviders = mockDataProviders.slice(0, 1); const { filterQuery } = combineQueries({ diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx index 6182fca6e2e993..e31f5aac137b87 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx @@ -7,7 +7,7 @@ import { isEmpty, isNumber, get } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import { StaticIndexPattern } from 'ui/index_patterns'; -import { Query, esFilters } from 'src/plugins/data/public'; +import { Query, esFilters } from '../../../../../../../src/plugins/data/public'; import { escapeQueryValue, convertToBuildEsQuery, EsQueryConfig } from '../../lib/keury'; @@ -113,13 +113,18 @@ export const combineQueries = ({ isEventViewer?: boolean; }): { filterQuery: string } | null => { const kuery: Query = { query: '', language: kqlQuery.language }; - if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEventViewer) { + if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { return null; } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; return { filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), }; + } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { + kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; } else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) { kuery.query = `(${kqlQuery.query}) and @timestamp >= ${start} and @timestamp <= ${end}`; return { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx index e4afef9a351e84..8c911b4ab06cb7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx @@ -9,6 +9,8 @@ import React, { useEffect, useCallback } from 'react'; import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; + import { WithSource } from '../../containers/source'; import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; import { timelineActions } from '../../store/actions'; @@ -40,6 +42,7 @@ interface StateReduxProps { columns: ColumnHeader[]; dataProviders?: DataProvider[]; end: number; + filters: esFilters.Filter[]; isLive: boolean; itemsPerPage?: number; itemsPerPageOptions?: number[]; @@ -137,6 +140,7 @@ const StatefulTimelineComponent = React.memo( createTimeline, dataProviders, end, + filters, flyoutHeaderHeight, flyoutHeight, id, @@ -252,6 +256,7 @@ const StatefulTimelineComponent = React.memo( columns={columns} dataProviders={dataProviders!} end={end} + filters={filters} flyoutHeaderHeight={flyoutHeaderHeight} flyoutHeight={flyoutHeight} id={id} @@ -295,6 +300,7 @@ const StatefulTimelineComponent = React.memo( prevProps.start === nextProps.start && isEqual(prevProps.columns, nextProps.columns) && isEqual(prevProps.dataProviders, nextProps.dataProviders) && + isEqual(prevProps.filters, nextProps.filters) && isEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && isEqual(prevProps.sort, nextProps.sort) ); @@ -314,6 +320,7 @@ const makeMapStateToProps = () => { const { columns, dataProviders, + filters, itemsPerPage, itemsPerPageOptions, kqlMode, @@ -322,10 +329,13 @@ const makeMapStateToProps = () => { } = timeline; const kqlQueryExpression = getKqlQueryTimeline(state, id); + const timelineFilter = kqlMode === 'filter' ? filters || [] : []; + return { columns, dataProviders, end: input.timerange.to, + filters: timelineFilter, id, isLive: input.policy.kind === 'interval', itemsPerPage, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx index 3b42ead64ad013..8c586cf958417e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx @@ -8,11 +8,19 @@ import { mount } from 'enzyme'; import * as React from 'react'; import { Provider as ReduxStoreProvider } from 'react-redux'; +import { useKibanaCore } from '../../../lib/compose/kibana_core'; import { mockGlobalState, apolloClientObservable } from '../../../mock'; +import { mockUiSettings } from '../../../mock/ui_settings'; import { createStore, State } from '../../../store'; import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; +const mockUseKibanaCore = useKibanaCore as jest.Mock; +jest.mock('../../../lib/compose/kibana_core'); +mockUseKibanaCore.mockImplementation(() => ({ + uiSettings: mockUiSettings, +})); + describe('Properties', () => { const usersViewing = ['elastic']; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx new file mode 100644 index 00000000000000..b78691fabdcbf1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx @@ -0,0 +1,406 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_FROM, DEFAULT_TO } from '../../../../common/constants'; +import { mockBrowserFields } from '../../../containers/source/mock'; +import { useKibanaCore } from '../../../lib/compose/kibana_core'; +import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; +import { mockIndexPattern, TestProviders } from '../../../mock'; +import { mockUiSettings } from '../../../mock/ui_settings'; +import { QueryBar } from '../../query_bar'; +import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; +import { buildGlobalQuery } from '../helpers'; + +import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; + +const mockUseKibanaCore = useKibanaCore as jest.Mock; +jest.mock('../../../lib/compose/kibana_core'); +mockUseKibanaCore.mockImplementation(() => ({ + uiSettings: mockUiSettings, + savedObjects: {}, +})); + +describe('Timeline QueryBar ', () => { + // We are doing that because we need to wrapped this component with redux + // and redux does not like to be updated and since we need to update our + // child component (BODY) and we do not want to scare anyone with this error + // we are hiding it!!! + // eslint-disable-next-line no-console + const originalError = console.error; + beforeAll(() => { + // eslint-disable-next-line no-console + console.error = (...args: string[]) => { + if (/ does not support changing `store` on the fly/.test(args[0])) { + return; + } + originalError.call(console, ...args); + }; + }); + + const mockApplyKqlFilterQuery = jest.fn(); + const mockSetFilters = jest.fn(); + const mockSetKqlFilterQueryDraft = jest.fn(); + const mockSetSavedQueryId = jest.fn(); + const mockUpdateReduxTime = jest.fn(); + + beforeEach(() => { + mockApplyKqlFilterQuery.mockClear(); + mockSetFilters.mockClear(); + mockSetKqlFilterQueryDraft.mockClear(); + mockSetSavedQueryId.mockClear(); + mockUpdateReduxTime.mockClear(); + }); + + test('check if we format the appropriate props to QueryBar', () => { + const wrapper = mount( + + + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + + expect(queryBarProps.dateRangeFrom).toEqual('now-24h'); + expect(queryBarProps.dateRangeTo).toEqual('now'); + expect(queryBarProps.filterQuery).toEqual({ query: 'here: query', language: 'kuery' }); + expect(queryBarProps.savedQuery).toEqual(null); + }); + + describe('#onChangeQuery', () => { + test(' is the only reference that changed when filterQueryDraft props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ filterQueryDraft: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onChangedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + }); + + describe('#onSubmitQuery', () => { + test(' is the only reference that changed when filterQuery props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + + test(' is only reference that changed when timelineId props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ timelineId: 'new-timeline' }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + }); + + describe('#onSavedQuery', () => { + test('is only reference that changed when dataProviders props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ dataProviders: mockDataProviders.slice(1, 0) }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + }); + + test('is only reference that changed when savedQueryId props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ + savedQueryId: 'new', + }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + }); + }); + + describe('#getDataProviderFilter', () => { + test('returns valid data provider filter with a simple bool data provider', () => { + const dataProvidersDsl = convertKueryToElasticSearchQuery( + buildGlobalQuery(mockDataProviders.slice(0, 1), mockBrowserFields), + mockIndexPattern + ); + const filter = getDataProviderFilter(dataProvidersDsl); + expect(filter).toEqual({ + $state: { + store: 'appState', + }, + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + name: 'Provider 1', + }, + }, + ], + }, + meta: { + alias: 'timeline-filter-drop-area', + controlledBy: 'timeline-filter-drop-area', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}', + }, + }); + }); + + test('returns valid data provider filter with an exists operator', () => { + const dataProvidersDsl = convertKueryToElasticSearchQuery( + buildGlobalQuery( + [ + { + id: `id-exists`, + name, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: '', + operator: ':*', + }, + and: [], + }, + ], + mockBrowserFields + ), + mockIndexPattern + ); + const filter = getDataProviderFilter(dataProvidersDsl); + expect(filter).toEqual({ + $state: { + store: 'appState', + }, + bool: { + minimum_should_match: 1, + should: [ + { + exists: { + field: 'host.name', + }, + }, + ], + }, + meta: { + alias: 'timeline-filter-drop-area', + controlledBy: 'timeline-filter-drop-area', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', + }, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx new file mode 100644 index 00000000000000..cb352059aaca4c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx @@ -0,0 +1,310 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual, isEmpty } from 'lodash/fp'; +import React, { memo, useCallback, useState, useEffect } from 'react'; +import { StaticIndexPattern } from 'ui/index_patterns'; +import { Query } from 'src/plugins/data/common/types'; +import { Subscription } from 'rxjs'; + +import { SavedQueryTimeFilter } from '../../../../../../../../src/legacy/core_plugins/data/public/search'; +import { SavedQuery } from '../../../../../../../../src/legacy/core_plugins/data/public'; +import { FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { + Filter, + FilterStateStore, +} from '../../../../../../../../src/plugins/data/common/es_query/filters'; + +import { BrowserFields } from '../../../containers/source'; +import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; +import { useKibanaCore } from '../../../lib/compose/kibana_core'; +import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; +import { KqlMode } from '../../../store/timeline/model'; +import { useSavedQueryServices } from '../../../utils/saved_query_services'; +import { DispatchUpdateReduxTime } from '../../super_date_picker'; +import { QueryBar } from '../../query_bar'; +import { DataProvider } from '../data_providers/data_provider'; +import { buildGlobalQuery } from '../helpers'; + +export interface QueryBarTimelineComponentProps { + applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; + browserFields: BrowserFields; + dataProviders: DataProvider[]; + filters: Filter[]; + filterQuery: KueryFilterQuery; + filterQueryDraft: KueryFilterQuery; + from: number; + fromStr: string; + kqlMode: KqlMode; + indexPattern: StaticIndexPattern; + isRefreshPaused: boolean; + refreshInterval: number; + savedQueryId: string | null; + setFilters: (filters: Filter[]) => void; + setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; + setSavedQueryId: (savedQueryId: string | null) => void; + timelineId: string; + to: number; + toStr: string; + updateReduxTime: DispatchUpdateReduxTime; +} + +const timelineFilterDropArea = 'timeline-filter-drop-area'; + +export const QueryBarTimeline = memo( + ({ + applyKqlFilterQuery, + browserFields, + dataProviders, + filters, + filterQuery, + filterQueryDraft, + from, + fromStr, + kqlMode, + indexPattern, + isRefreshPaused, + savedQueryId, + setFilters, + setKqlFilterQueryDraft, + setSavedQueryId, + refreshInterval, + timelineId, + to, + toStr, + updateReduxTime, + }) => { + const [dateRangeFrom, setDateRangeFrom] = useState( + fromStr != null ? fromStr : new Date(from).toISOString() + ); + const [dateRangeTo, setDateRangTo] = useState( + toStr != null ? toStr : new Date(to).toISOString() + ); + + const [savedQuery, setSavedQuery] = useState(null); + const [filterQueryConverted, setFilterQueryConverted] = useState({ + query: filterQuery != null ? filterQuery.expression : '', + language: filterQuery != null ? filterQuery.kind : 'kuery', + }); + const [queryBarFilters, setQueryBarFilters] = useState([]); + const [dataProvidersDsl, setDataProvidersDsl] = useState( + convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern) + ); + const core = useKibanaCore(); + const [filterManager] = useState(new FilterManager(core.uiSettings)); + + const savedQueryServices = useSavedQueryServices(); + + useEffect(() => { + let isSubscribed = true; + const subscriptions = new Subscription(); + filterManager.setFilters(filters); + + subscriptions.add( + filterManager.getUpdates$().subscribe({ + next: () => { + if (isSubscribed) { + const filterWithoutDropArea = filterManager + .getFilters() + .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); + setFilters(filterWithoutDropArea); + setQueryBarFilters(filterWithoutDropArea); + } + }, + }) + ); + + return () => { + isSubscribed = false; + subscriptions.unsubscribe(); + }; + }, []); + + useEffect(() => { + const filterWithoutDropArea = filterManager + .getFilters() + .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); + if (!isEqual(filters, filterWithoutDropArea)) { + filterManager.setFilters(filters); + } + }, [filters]); + + useEffect(() => { + setFilterQueryConverted({ + query: filterQuery != null ? filterQuery.expression : '', + language: filterQuery != null ? filterQuery.kind : 'kuery', + }); + }, [filterQuery]); + + useEffect(() => { + setDataProvidersDsl( + convertKueryToElasticSearchQuery( + buildGlobalQuery(dataProviders, browserFields), + indexPattern + ) + ); + }, [dataProviders, browserFields, indexPattern]); + + useEffect(() => { + if (fromStr != null && toStr != null) { + setDateRangeFrom(fromStr); + setDateRangTo(toStr); + } else if (from != null && to != null) { + setDateRangeFrom(new Date(from).toISOString()); + setDateRangTo(new Date(to).toISOString()); + } + }, [from, fromStr, to, toStr]); + + useEffect(() => { + let isSubscribed = true; + async function setSavedQueryByServices() { + if (savedQueryId != null && savedQueryServices != null) { + const mySavedQuery = await savedQueryServices.getSavedQuery(savedQueryId); + if (isSubscribed) { + setSavedQuery({ + ...mySavedQuery, + attributes: { + ...mySavedQuery.attributes, + filters: filters.filter(f => f.meta.controlledBy !== timelineFilterDropArea), + }, + }); + } + } else if (isSubscribed) { + setSavedQuery(null); + } + } + setSavedQueryByServices(); + return () => { + isSubscribed = false; + }; + }, [savedQueryId]); + + const onChangedQuery = useCallback( + (newQuery: Query) => { + if ( + filterQueryDraft == null || + (filterQueryDraft != null && filterQueryDraft.expression !== newQuery.query) || + filterQueryDraft.kind !== newQuery.language + ) { + setKqlFilterQueryDraft( + newQuery.query as string, + newQuery.language as KueryFilterQueryKind + ); + } + }, + [filterQueryDraft] + ); + + const onSubmitQuery = useCallback( + (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { + if ( + filterQuery == null || + (filterQuery != null && filterQuery.expression !== newQuery.query) || + filterQuery.kind !== newQuery.language + ) { + setKqlFilterQueryDraft( + newQuery.query as string, + newQuery.language as KueryFilterQueryKind + ); + applyKqlFilterQuery(newQuery.query as string, newQuery.language as KueryFilterQueryKind); + } + if (timefilter != null) { + const isQuickSelection = timefilter.from.includes('now') || timefilter.to.includes('now'); + + updateReduxTime({ + id: 'timeline', + end: timefilter.to, + start: timefilter.from, + isInvalid: false, + isQuickSelection, + timelineId, + }); + } + }, + [filterQuery, timelineId] + ); + + const onSavedQuery = useCallback( + (newSavedQuery: SavedQuery | null) => { + if (newSavedQuery != null) { + if (newSavedQuery.id !== savedQueryId) { + setSavedQueryId(newSavedQuery.id); + } + if (savedQueryServices != null && dataProvidersDsl !== '') { + const dataProviderFilterExists = + newSavedQuery.attributes.filters != null + ? newSavedQuery.attributes.filters.findIndex( + f => f.meta.controlledBy === timelineFilterDropArea + ) + : -1; + savedQueryServices.saveQuery( + { + ...newSavedQuery.attributes, + filters: + newSavedQuery.attributes.filters != null + ? dataProviderFilterExists > -1 + ? [ + ...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists), + getDataProviderFilter(dataProvidersDsl), + ...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1), + ] + : [ + ...newSavedQuery.attributes.filters, + getDataProviderFilter(dataProvidersDsl), + ] + : [], + }, + { + overwrite: true, + } + ); + } + } else { + setSavedQueryId(null); + } + }, + [dataProvidersDsl, savedQueryId, savedQueryServices] + ); + + return ( + + ); + } +); + +export const getDataProviderFilter = (dataProviderDsl: string): Filter => { + const dslObject = JSON.parse(dataProviderDsl); + const key = Object.keys(dslObject); + return { + ...dslObject, + meta: { + alias: timelineFilterDropArea, + controlledBy: timelineFilterDropArea, + negate: false, + disabled: false, + type: 'custom', + key: isEmpty(key) ? 'bool' : key[0], + value: dataProviderDsl, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx index ec491fe50407a2..4af6178a7223b2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -4,43 +4,69 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; +import { getOr, isEqual } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect } from 'react-redux'; -import { ActionCreator } from 'typescript-fsa'; +import { Dispatch } from 'redux'; import { StaticIndexPattern } from 'ui/index_patterns'; +import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { BrowserFields } from '../../../containers/source'; import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { KueryFilterQuery, SerializedFilterQuery, State, timelineSelectors } from '../../../store'; - -import { SearchOrFilter } from './search_or_filter'; +import { + KueryFilterQuery, + SerializedFilterQuery, + State, + timelineSelectors, + inputsModel, + inputsSelectors, +} from '../../../store'; import { timelineActions } from '../../../store/actions'; import { KqlMode, TimelineModel } from '../../../store/timeline/model'; +import { DispatchUpdateReduxTime, dispatchUpdateReduxTime } from '../../super_date_picker'; +import { DataProvider } from '../data_providers/data_provider'; +import { SearchOrFilter } from './search_or_filter'; interface OwnProps { + browserFields: BrowserFields; indexPattern: StaticIndexPattern; timelineId: string; } interface StateReduxProps { + dataProviders: DataProvider[]; + filters: esFilters.Filter[]; + filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; - isFilterQueryDraftValid: boolean; - kqlMode?: KqlMode; + from: number; + fromStr: string; + isRefreshPaused: boolean; + kqlMode: KqlMode; + refreshInterval: number; + savedQueryId: string | null; + to: number; + toStr: string; } interface DispatchProps { - applyKqlFilterQuery: ActionCreator<{ + applyKqlFilterQuery: ({ + id, + filterQuery, + }: { id: string; filterQuery: SerializedFilterQuery; - }>; - updateKqlMode: ActionCreator<{ - id: string; - kqlMode: KqlMode; - }>; - setKqlFilterQueryDraft: ActionCreator<{ + }) => void; + updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => void; + setKqlFilterQueryDraft: ({ + id, + filterQueryDraft, + }: { id: string; filterQueryDraft: KueryFilterQuery; - }>; + }) => void; + setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) => void; + setFilters: ({ id, filters }: { id: string; filters: esFilters.Filter[] }) => void; + updateReduxTime: DispatchUpdateReduxTime; } type Props = OwnProps & StateReduxProps & DispatchProps; @@ -48,21 +74,34 @@ type Props = OwnProps & StateReduxProps & DispatchProps; const StatefulSearchOrFilterComponent = React.memo( ({ applyKqlFilterQuery, + browserFields, + dataProviders, + filters, + filterQuery, filterQueryDraft, + from, + fromStr, indexPattern, - isFilterQueryDraftValid, + isRefreshPaused, kqlMode, + refreshInterval, + savedQueryId, + setFilters, setKqlFilterQueryDraft, + setSavedQueryId, timelineId, + to, + toStr, updateKqlMode, + updateReduxTime, }) => { const applyFilterQueryFromKueryExpression = useCallback( - (expression: string) => + (expression: string, kind) => applyKqlFilterQuery({ id: timelineId, filterQuery: { kuery: { - kind: 'kuery', + kind, expression, }, serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), @@ -72,29 +111,80 @@ const StatefulSearchOrFilterComponent = React.memo( ); const setFilterQueryDraftFromKueryExpression = useCallback( - (expression: string) => + (expression: string, kind) => setKqlFilterQueryDraft({ id: timelineId, filterQueryDraft: { - kind: 'kuery', + kind, expression, }, }), [timelineId] ); + const setFiltersInTimeline = useCallback( + (newFilters: esFilters.Filter[]) => + setFilters({ + id: timelineId, + filters: newFilters, + }), + [timelineId] + ); + + const setSavedQueryInTimeline = useCallback( + (newSavedQueryId: string | null) => + setSavedQueryId({ + id: timelineId, + savedQueryId: newSavedQueryId, + }), + [timelineId] + ); + return ( ); + }, + (prevProps, nextProps) => { + return ( + prevProps.from === nextProps.from && + prevProps.fromStr === nextProps.fromStr && + prevProps.to === nextProps.to && + prevProps.toStr === nextProps.toStr && + prevProps.isRefreshPaused === nextProps.isRefreshPaused && + prevProps.refreshInterval === nextProps.refreshInterval && + prevProps.timelineId === nextProps.timelineId && + isEqual(prevProps.browserFields, nextProps.browserFields) && + isEqual(prevProps.dataProviders, nextProps.dataProviders) && + isEqual(prevProps.filters, nextProps.filters) && + isEqual(prevProps.filterQuery, nextProps.filterQuery) && + isEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && + isEqual(prevProps.indexPattern, nextProps.indexPattern) && + isEqual(prevProps.kqlMode, nextProps.kqlMode) && + isEqual(prevProps.savedQueryId, nextProps.savedQueryId) && + isEqual(prevProps.timelineId, nextProps.timelineId) + ); } ); StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent'; @@ -102,20 +192,62 @@ StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent'; const makeMapStateToProps = () => { const getTimeline = timelineSelectors.getTimelineByIdSelector(); const getKqlFilterQueryDraft = timelineSelectors.getKqlFilterQueryDraftSelector(); - const isFilterQueryDraftValid = timelineSelectors.isFilterQueryDraftValidSelector(); + const getKqlFilterQuery = timelineSelectors.getKqlFilterKuerySelector(); + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getInputsPolicy = inputsSelectors.getTimelinePolicySelector(); const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timeline: TimelineModel | {} = getTimeline(state, timelineId); + const timeline: TimelineModel = getTimeline(state, timelineId); + const input: inputsModel.InputsRange = getInputsTimeline(state); + const policy: inputsModel.Policy = getInputsPolicy(state); return { + dataProviders: timeline.dataProviders, + filterQuery: getKqlFilterQuery(state, timelineId), filterQueryDraft: getKqlFilterQueryDraft(state, timelineId), - isFilterQueryDraftValid: isFilterQueryDraftValid(state, timelineId), + filters: timeline.filters, + from: input.timerange.from, + fromStr: input.timerange.fromStr, + isRefreshPaused: policy.kind === 'manual', kqlMode: getOr('filter', 'kqlMode', timeline), + refreshInterval: policy.duration, + savedQueryId: getOr(null, 'savedQueryId', timeline), + to: input.timerange.to, + toStr: input.timerange.toStr, }; }; return mapStateToProps; }; -export const StatefulSearchOrFilter = connect(makeMapStateToProps, { - applyKqlFilterQuery: timelineActions.applyKqlFilterQuery, - setKqlFilterQueryDraft: timelineActions.setKqlFilterQueryDraft, - updateKqlMode: timelineActions.updateKqlMode, -})(StatefulSearchOrFilterComponent); +const mapDispatchToProps = (dispatch: Dispatch) => ({ + applyKqlFilterQuery: ({ id, filterQuery }: { id: string; filterQuery: SerializedFilterQuery }) => + dispatch( + timelineActions.applyKqlFilterQuery({ + id, + filterQuery, + }) + ), + updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => + dispatch(timelineActions.updateKqlMode({ id, kqlMode })), + setKqlFilterQueryDraft: ({ + id, + filterQueryDraft, + }: { + id: string; + filterQueryDraft: KueryFilterQuery; + }) => + dispatch( + timelineActions.setKqlFilterQueryDraft({ + id, + filterQueryDraft, + }) + ), + setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) => + dispatch(timelineActions.setSavedQueryId({ id, savedQueryId })), + setFilters: ({ id, filters }: { id: string; filters: esFilters.Filter[] }) => + dispatch(timelineActions.setFilters({ id, filters })), + updateReduxTime: dispatchUpdateReduxTime(dispatch), +}); + +export const StatefulSearchOrFilter = connect( + makeMapStateToProps, + mapDispatchToProps +)(StatefulSearchOrFilterComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 08ab44317e82f4..db8909adda239b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -10,13 +10,16 @@ import { pure } from 'recompose'; import styled, { injectGlobal } from 'styled-components'; import { StaticIndexPattern } from 'ui/index_patterns'; -import { KueryAutocompletion } from '../../../containers/kuery_autocompletion'; -import { KueryFilterQuery } from '../../../store'; -import { AutocompleteField } from '../../autocomplete_field'; +import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { BrowserFields } from '../../../containers/source'; +import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; +import { KqlMode } from '../../../store/timeline/model'; +import { DataProvider } from '../data_providers/data_provider'; +import { QueryBarTimeline } from '../query_bar'; -import { getPlaceholderText, modes, options } from './helpers'; +import { options } from './helpers'; import * as i18n from './translations'; -import { KqlMode } from '../../../store/timeline/model'; +import { DispatchUpdateReduxTime } from '../../super_date_picker'; const timelineSelectModeItemsClassName = 'timelineSelectModeItemsClassName'; const searchOrFilterPopoverClassName = 'searchOrFilterPopover'; @@ -39,19 +42,40 @@ injectGlobal` `; interface Props { - applyKqlFilterQuery: (expression: string) => void; + applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; + browserFields: BrowserFields; + dataProviders: DataProvider[]; + filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; + from: number; + fromStr: string; indexPattern: StaticIndexPattern; - isFilterQueryDraftValid: boolean; + isRefreshPaused: boolean; kqlMode: KqlMode; timelineId: string; updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => void; - setKqlFilterQueryDraft: (expression: string) => void; + refreshInterval: number; + setFilters: (filters: esFilters.Filter[]) => void; + setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; + setSavedQueryId: (savedQueryId: string | null) => void; + filters: esFilters.Filter[]; + savedQueryId: string | null; + to: number; + toStr: string; + updateReduxTime: DispatchUpdateReduxTime; } const SearchOrFilterContainer = styled.div` margin: 5px 0 10px 0; user-select: none; + .globalQueryBar { + padding: 0px; + .kbnQueryBar { + div:first-child { + margin-right: 0px; + } + } + } `; SearchOrFilterContainer.displayName = 'SearchOrFilterContainer'; @@ -65,13 +89,26 @@ ModeFlexItem.displayName = 'ModeFlexItem'; export const SearchOrFilter = pure( ({ applyKqlFilterQuery, + browserFields, + dataProviders, indexPattern, - isFilterQueryDraftValid, + isRefreshPaused, + filters, + filterQuery, filterQueryDraft, + from, + fromStr, kqlMode, timelineId, + refreshInterval, + savedQueryId, + setFilters, setKqlFilterQueryDraft, + setSavedQueryId, + to, + toStr, updateKqlMode, + updateReduxTime, }) => ( @@ -90,22 +127,28 @@ export const SearchOrFilter = pure( - - - {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( - - )} - - + diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index 700489f47d0cf3..a52d4ce38ccb2f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -33,6 +33,7 @@ const mockUseKibanaCore = useKibanaCore as jest.Mock; jest.mock('../../lib/compose/kibana_core'); mockUseKibanaCore.mockImplementation(() => ({ uiSettings: mockUiSettings, + savedObjects: {}, })); describe('Timeline', () => { @@ -58,6 +59,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -93,6 +95,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -131,6 +134,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -169,6 +173,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -212,6 +217,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -257,6 +263,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -310,6 +317,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -367,6 +375,7 @@ describe('Timeline', () => { id="foo" dataProviders={mockDataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -427,6 +436,7 @@ describe('Timeline', () => { id="foo" dataProviders={dataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -477,6 +487,7 @@ describe('Timeline', () => { id="foo" dataProviders={dataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -533,6 +544,7 @@ describe('Timeline', () => { id="foo" dataProviders={dataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} @@ -593,6 +605,7 @@ describe('Timeline', () => { id="foo" dataProviders={dataProviders} end={endDate} + filters={[]} flyoutHeight={testFlyoutHeight} flyoutHeaderHeight={flyoutHeaderHeight} indexPattern={indexPattern} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index fb62b636398c23..d3a6c77db64fe4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -11,6 +11,7 @@ import * as React from 'react'; import styled from 'styled-components'; import { StaticIndexPattern } from 'ui/index_patterns'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../containers/source'; import { TimelineQuery } from '../../containers/timeline'; import { Direction } from '../../graphql/types'; @@ -61,6 +62,7 @@ interface Props { columns: ColumnHeader[]; dataProviders: DataProvider[]; end: number; + filters: esFilters.Filter[]; flyoutHeaderHeight: number; flyoutHeight: number; id: string; @@ -91,6 +93,7 @@ export const Timeline = React.memo( columns, dataProviders, end, + filters, flyoutHeaderHeight, flyoutHeight, id, @@ -119,7 +122,7 @@ export const Timeline = React.memo( dataProviders, indexPattern, browserFields, - filters: [], + filters, kqlQuery: { query: kqlQueryExpression, language: 'kuery' }, kqlMode, start, diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts index f8d615ee9a7da4..eebb4a349dab43 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts @@ -72,6 +72,27 @@ export const oneTimelineQuery = gql` userName favoriteDate } + filters { + meta { + alias + controlledBy + disabled + field + formattedValue + index + key + negate + params + type + value + } + query + exists + match_all + missing + range + script + } kqlMode kqlQuery { filterQuery { @@ -107,6 +128,7 @@ export const oneTimelineQuery = gql` version } title + savedQueryId sort { columnId sortDirection diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts index a170e4d53fd7bb..68b749064dc0cf 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts @@ -60,6 +60,27 @@ export const persistTimelineMutation = gql` userName favoriteDate } + filters { + meta { + alias + controlledBy + disabled + field + formattedValue + index + key + negate + params + type + value + } + query + exists + match_all + missing + range + script + } kqlMode kqlQuery { filterQuery { @@ -75,6 +96,7 @@ export const persistTimelineMutation = gql` start end } + savedQueryId sort { columnId sortDirection diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 8732b61e5c0eb0..9bde4bf47fff07 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -9044,18 +9044,6 @@ "name": "TimelineResult", "description": "", "fields": [ - { - "name": "savedObjectId", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "columns", "description": "", @@ -9072,6 +9060,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "created", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdBy", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "dataProviders", "description": "", @@ -9136,6 +9140,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "filters", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "FilterTimelineResult", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "kqlMode", "description": "", @@ -9217,7 +9237,7 @@ "deprecationReason": null }, { - "name": "title", + "name": "savedQueryId", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, @@ -9225,23 +9245,27 @@ "deprecationReason": null }, { - "name": "sort", + "name": "savedObjectId", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "SortTimelineResult", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "created", + "name": "sort", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { "kind": "OBJECT", "name": "SortTimelineResult", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "createdBy", + "name": "title", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, @@ -9577,6 +9601,172 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "FilterTimelineResult", + "description": "", + "fields": [ + { + "name": "exists", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "meta", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "FilterMetaTimelineResult", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "match_all", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "missing", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "query", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "range", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "script", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FilterMetaTimelineResult", + "description": "", + "fields": [ + { + "name": "alias", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "controlledBy", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "disabled", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "field", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "formattedValue", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "index", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "key", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "negate", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "params", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "value", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "SerializedFilterQueryResult", @@ -10175,6 +10365,20 @@ "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null }, + { + "name": "filters", + "description": "", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "FilterTimelineInput", "ofType": null } + } + }, + "defaultValue": null + }, { "name": "kqlMode", "description": "", @@ -10203,6 +10407,12 @@ "type": { "kind": "INPUT_OBJECT", "name": "DateRangePickerInput", "ofType": null }, "defaultValue": null }, + { + "name": "savedQueryId", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, { "name": "sort", "description": "", @@ -10401,6 +10611,136 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "FilterTimelineInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "exists", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "meta", + "description": "", + "type": { "kind": "INPUT_OBJECT", "name": "FilterMetaTimelineInput", "ofType": null }, + "defaultValue": null + }, + { + "name": "match_all", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "missing", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "query", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "range", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "script", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "FilterMetaTimelineInput", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "alias", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "controlledBy", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "disabled", + "description": "", + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, + "defaultValue": null + }, + { + "name": "field", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "formattedValue", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "index", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "key", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "negate", + "description": "", + "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, + "defaultValue": null + }, + { + "name": "params", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "type", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "value", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "SerializedFilterQueryInput", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index a1b259b876a142..833102a0d00bc1 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -122,6 +122,8 @@ export interface TimelineInput { description?: Maybe; + filters?: Maybe; + kqlMode?: Maybe; kqlQuery?: Maybe; @@ -130,6 +132,8 @@ export interface TimelineInput { dateRange?: Maybe; + savedQueryId?: Maybe; + sort?: Maybe; } @@ -185,6 +189,46 @@ export interface QueryMatchInput { operator?: Maybe; } +export interface FilterTimelineInput { + exists?: Maybe; + + meta?: Maybe; + + match_all?: Maybe; + + missing?: Maybe; + + query?: Maybe; + + range?: Maybe; + + script?: Maybe; +} + +export interface FilterMetaTimelineInput { + alias?: Maybe; + + controlledBy?: Maybe; + + disabled?: Maybe; + + field?: Maybe; + + formattedValue?: Maybe; + + index?: Maybe; + + key?: Maybe; + + negate?: Maybe; + + params?: Maybe; + + type?: Maybe; + + value?: Maybe; +} + export interface SerializedFilterQueryInput { filterQuery?: Maybe; } @@ -1760,10 +1804,12 @@ export interface SayMyName { } export interface TimelineResult { - savedObjectId: string; - columns?: Maybe; + created?: Maybe; + + createdBy?: Maybe; + dataProviders?: Maybe; dateRange?: Maybe; @@ -1774,6 +1820,8 @@ export interface TimelineResult { favorite?: Maybe; + filters?: Maybe; + kqlMode?: Maybe; kqlQuery?: Maybe; @@ -1786,13 +1834,13 @@ export interface TimelineResult { pinnedEventsSaveObject?: Maybe; - title?: Maybe; + savedQueryId?: Maybe; - sort?: Maybe; + savedObjectId: string; - created?: Maybe; + sort?: Maybe; - createdBy?: Maybe; + title?: Maybe; updated?: Maybe; @@ -1867,6 +1915,46 @@ export interface FavoriteTimelineResult { favoriteDate?: Maybe; } +export interface FilterTimelineResult { + exists?: Maybe; + + meta?: Maybe; + + match_all?: Maybe; + + missing?: Maybe; + + query?: Maybe; + + range?: Maybe; + + script?: Maybe; +} + +export interface FilterMetaTimelineResult { + alias?: Maybe; + + controlledBy?: Maybe; + + disabled?: Maybe; + + field?: Maybe; + + formattedValue?: Maybe; + + index?: Maybe; + + key?: Maybe; + + negate?: Maybe; + + params?: Maybe; + + type?: Maybe; + + value?: Maybe; +} + export interface SerializedFilterQueryResult { filterQuery?: Maybe; } @@ -4874,6 +4962,8 @@ export namespace GetOneTimeline { favorite: Maybe; + filters: Maybe; + kqlMode: Maybe; kqlQuery: Maybe; @@ -4888,6 +4978,8 @@ export namespace GetOneTimeline { title: Maybe; + savedQueryId: Maybe; + sort: Maybe; created: Maybe; @@ -5029,6 +5121,50 @@ export namespace GetOneTimeline { favoriteDate: Maybe; }; + export type Filters = { + __typename?: 'FilterTimelineResult'; + + meta: Maybe; + + query: Maybe; + + exists: Maybe; + + match_all: Maybe; + + missing: Maybe; + + range: Maybe; + + script: Maybe; + }; + + export type Meta = { + __typename?: 'FilterMetaTimelineResult'; + + alias: Maybe; + + controlledBy: Maybe; + + disabled: Maybe; + + field: Maybe; + + formattedValue: Maybe; + + index: Maybe; + + key: Maybe; + + negate: Maybe; + + params: Maybe; + + type: Maybe; + + value: Maybe; + }; + export type KqlQuery = { __typename?: 'SerializedFilterQueryResult'; @@ -5142,6 +5278,8 @@ export namespace PersistTimelineMutation { favorite: Maybe; + filters: Maybe; + kqlMode: Maybe; kqlQuery: Maybe; @@ -5150,6 +5288,8 @@ export namespace PersistTimelineMutation { dateRange: Maybe; + savedQueryId: Maybe; + sort: Maybe; created: Maybe; @@ -5257,6 +5397,50 @@ export namespace PersistTimelineMutation { favoriteDate: Maybe; }; + export type Filters = { + __typename?: 'FilterTimelineResult'; + + meta: Maybe; + + query: Maybe; + + exists: Maybe; + + match_all: Maybe; + + missing: Maybe; + + range: Maybe; + + script: Maybe; + }; + + export type Meta = { + __typename?: 'FilterMetaTimelineResult'; + + alias: Maybe; + + controlledBy: Maybe; + + disabled: Maybe; + + field: Maybe; + + formattedValue: Maybe; + + index: Maybe; + + key: Maybe; + + negate: Maybe; + + params: Maybe; + + type: Maybe; + + value: Maybe; + }; + export type KqlQuery = { __typename?: 'SerializedFilterQueryResult'; diff --git a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts index e9f4c95a80b743..81ceae68c5fb9d 100644 --- a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts +++ b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { buildEsQuery, fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { buildEsQuery, fromKueryExpression, toElasticsearchQuery, JsonObject } from '@kbn/es-query'; import { isEmpty, isString, flow } from 'lodash/fp'; import { StaticIndexPattern } from 'ui/index_patterns'; import { Query } from 'src/plugins/data/common'; + import { esFilters } from '../../../../../../../src/plugins/data/public'; import { KueryFilterQuery } from '../../store'; @@ -25,6 +26,19 @@ export const convertKueryToElasticSearchQuery = ( } }; +export const convertKueryToDslFilter = ( + kueryExpression: string, + indexPattern: StaticIndexPattern +): JsonObject => { + try { + return kueryExpression + ? toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern) + : {}; + } catch (err) { + return {}; + } +}; + export const escapeQueryValue = (val: number | string = ''): string | number => { if (isString(val)) { if (isEmpty(val)) { diff --git a/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts b/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts index 9ba569cfec2280..23f1f0e86dd6aa 100644 --- a/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts +++ b/x-pack/legacy/plugins/siem/public/mock/kibana_config.ts @@ -9,6 +9,7 @@ import { DEFAULT_BYTES_FORMAT, DEFAULT_KBN_VERSION, DEFAULT_TIMEZONE_BROWSER, + DEFAULT_TIMEPICKER_QUICK_RANGES, } from '../../common/constants'; export interface MockFrameworks { @@ -29,6 +30,61 @@ export const getMockKibanaUiSetting = (config: MockFrameworks) => (key: string) return ['8.0.0']; } else if (key === DEFAULT_TIMEZONE_BROWSER) { return config && config.timezone ? [config.timezone] : ['America/New_York']; + } else if (key === DEFAULT_TIMEPICKER_QUICK_RANGES) { + return [ + [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + { + from: 'now-15m', + to: 'now', + display: 'Last 15 minutes', + }, + { + from: 'now-30m', + to: 'now', + display: 'Last 30 minutes', + }, + { + from: 'now-1h', + to: 'now', + display: 'Last 1 hour', + }, + { + from: 'now-24h', + to: 'now', + display: 'Last 24 hours', + }, + { + from: 'now-7d', + to: 'now', + display: 'Last 7 days', + }, + { + from: 'now-30d', + to: 'now', + display: 'Last 30 days', + }, + { + from: 'now-90d', + to: 'now', + display: 'Last 90 days', + }, + { + from: 'now-1y', + to: 'now', + display: 'Last 1 year', + }, + ], + ]; } return [null]; }; diff --git a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx index 75df1df59187f8..d4a7bb1f425d4b 100644 --- a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx +++ b/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx @@ -18,8 +18,14 @@ import { Store } from 'redux'; import { BehaviorSubject } from 'rxjs'; import { ThemeProvider } from 'styled-components'; +import { CoreStart } from 'src/core/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; + import { createStore, State } from '../store'; import { mockGlobalState } from './global_state'; +import { mockUiSettings } from './ui_settings'; + +jest.mock('ui/new_platform'); const state: State = mockGlobalState; @@ -36,17 +42,57 @@ export const apolloClient = new ApolloClient({ export const apolloClientObservable = new BehaviorSubject(apolloClient); +const services = { + uiSettings: mockUiSettings, + savedObjects: {} as CoreStart['savedObjects'], + notifications: {} as CoreStart['notifications'], + docLinks: { + links: { + query: { + kueryQuerySyntax: '', + }, + }, + } as CoreStart['docLinks'], + http: {} as CoreStart['http'], + overlays: {} as CoreStart['overlays'], + storage: { + get: () => {}, + }, +}; + +const localStorageMock = () => { + let store: Record = {}; + + return { + getItem: (key: string) => { + return store[key] || null; + }, + setItem: (key: string, value: unknown) => { + store[key] = value; + }, + clear() { + store = {}; + }, + }; +}; + +Object.defineProperty(window, 'localStorage', { + value: localStorageMock(), +}); + /** A utility for wrapping children in the providers required to run most tests */ export const TestProviders = pure( ({ children, store = createStore(state, apolloClientObservable), onDragEnd = jest.fn() }) => ( - - - ({ eui: euiDarkVars, darkMode: true })}> - {children} - - - + + + + ({ eui: euiDarkVars, darkMode: true })}> + {children} + + + + ) ); diff --git a/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts b/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts index c7803e99f3857f..6c6411c6bda533 100644 --- a/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts +++ b/x-pack/legacy/plugins/siem/public/mock/ui_settings.ts @@ -9,6 +9,7 @@ import { DEFAULT_SIEM_TIME_RANGE, DEFAULT_SIEM_REFRESH_INTERVAL, DEFAULT_INDEX_KEY, + DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ, DEFAULT_DARK_MODE, DEFAULT_TIME_RANGE, @@ -40,6 +41,8 @@ chrome.getUiSettingsClient().get.mockImplementation((key: string) => { return defaultIndexPattern; case DEFAULT_DATE_FORMAT_TZ: return 'Asia/Taipei'; + case DEFAULT_DATE_FORMAT: + return 'MMM D, YYYY @ HH:mm:ss.SSS'; case DEFAULT_DARK_MODE: return false; default: @@ -62,6 +65,9 @@ export const mockUiSettings = { get: (item: Config) => { return mockUiSettings[item]; }, + get$: () => ({ + subscribe: jest.fn(), + }), 'query:allowLeadingWildcards': true, 'query:queryString:options': {}, 'courier:ignoreFilterIfFieldNotInIndex': true, diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx index 6ceebc1708b185..f136ff72c906da 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx @@ -39,10 +39,13 @@ jest.mock('../../../containers/source', () => ({ })); // Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar +// For now let's forget about SiemSearchBar and QueryBar jest.mock('../../../components/search_bar', () => ({ SiemSearchBar: () => null, })); +jest.mock('../../../components/query_bar', () => ({ + QueryBar: () => null, +})); describe('body', () => { const scenariosMap = { diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx index d2c9822889c265..2d0df0b6e00334 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx @@ -38,10 +38,13 @@ jest.mock('ui/documentation_links', () => ({ })); // Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar +// For now let's forget about SiemSearchBar and QueryBar jest.mock('../../components/search_bar', () => ({ SiemSearchBar: () => null, })); +jest.mock('../../components/query_bar', () => ({ + QueryBar: () => null, +})); let localSource: Array<{ request: {}; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx index b56b9d931af47c..9e599bcfedff6b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx @@ -38,10 +38,13 @@ mockUseKibanaCore.mockImplementation(() => ({ })); // Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar +// For now let's forget about SiemSearchBar and QueryBar jest.mock('../../../components/search_bar', () => ({ SiemSearchBar: () => null, })); +jest.mock('../../../components/query_bar', () => ({ + QueryBar: () => null, +})); let localSource: Array<{ request: {}; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx index a10118dc6ca6fe..a374b0082f2810 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx @@ -33,10 +33,13 @@ mockUseKibanaCore.mockImplementation(() => ({ })); // Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar +// For now let's forget about SiemSearchBar and QueryBar jest.mock('../../components/search_bar', () => ({ SiemSearchBar: () => null, })); +jest.mock('../../components/query_bar', () => ({ + QueryBar: () => null, +})); let localSource: Array<{ request: {}; diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts b/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts index cb2d357b740078..a2f061dc648d5d 100644 --- a/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts @@ -73,3 +73,6 @@ export const globalFiltersQuerySelector = () => createSelector(selectGlobal, global => global.filters || []); export const getTimelineSelector = () => createSelector(selectTimeline, timeline => timeline); + +export const getTimelinePolicySelector = () => + createSelector(selectTimeline, timeline => timeline.policy); diff --git a/x-pack/legacy/plugins/siem/public/store/model.ts b/x-pack/legacy/plugins/siem/public/store/model.ts index 6f5bd327a10180..6f04f22866be5b 100644 --- a/x-pack/legacy/plugins/siem/public/store/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/model.ts @@ -10,8 +10,10 @@ export { hostsModel } from './hosts'; export { dragAndDropModel } from './drag_and_drop'; export { networkModel } from './network'; +export type KueryFilterQueryKind = 'kuery' | 'lucene'; + export interface KueryFilterQuery { - kind: 'kuery'; + kind: KueryFilterQueryKind; expression: string; } diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts b/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts index 9729505c4e944a..931d3e26172cdc 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts @@ -6,6 +6,7 @@ import actionCreatorFactory from 'typescript-fsa'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header'; import { Sort } from '../../components/timeline/body/sort'; import { @@ -187,3 +188,13 @@ export const updateAutoSaveMsg = actionCreator<{ }>('UPDATE_AUTO_SAVE'); export const showCallOutUnauthorizedMsg = actionCreator('SHOW_CALL_OUT_UNAUTHORIZED_MSG'); + +export const setSavedQueryId = actionCreator<{ + id: string; + savedQueryId: string | null; +}>('SET_TIMELINE_SAVED_QUERY'); + +export const setFilters = actionCreator<{ + id: string; + filters: esFilters.Filter[]; +}>('SET_TIMELINE_FILTERS'); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts new file mode 100644 index 00000000000000..85d2e624c28079 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts @@ -0,0 +1,283 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimelineModel } from './model'; +import { Direction } from '../../graphql/types'; +import { convertTimelineAsInput } from './epic'; + +import { esFilters } from '../../../../../../../src/plugins/data/public'; +import { FilterStateStore } from '../../../../../../../src/plugins/data/common/es_query/filters'; + +describe('Epic Timeline', () => { + describe('#convertTimelineAsInput ', () => { + test('should return a TimelineInput instead of TimelineModel ', () => { + const timelineModel: TimelineModel = { + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + dataProviders: [ + { + id: 'hosts-table-hostName-DESKTOP-QBBSCUT', + name: 'DESKTOP-QBBSCUT', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'DESKTOP-QBBSCUT', + operator: ':', + }, + and: [ + { + id: + 'plain-column-renderer-data-provider-hosts-page-event_module-CQg7I24BHe9nqdOi_LYL-event_module-endgame', + name: 'event.module: endgame', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'event.module', + value: 'endgame', + operator: ':', + }, + }, + ], + }, + ], + description: '', + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + filters: [ + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { query: 'file' }, + type: 'phrase', + }, + query: { match_phrase: { 'event.category': 'file' } }, + }, + { + $state: { store: FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: '@timestamp', + negate: false, + type: 'exists', + value: 'exists', + }, + exists: { field: '@timestamp' }, + } as esFilters.Filter, + ], + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { kind: 'kuery', expression: 'endgame.user_name : "zeus" ' }, + serializedQuery: + '{"bool":{"should":[{"match_phrase":{"endgame.user_name":"zeus"}}],"minimum_should_match":1}}', + }, + filterQueryDraft: { kind: 'kuery', expression: 'endgame.user_name : "zeus" ' }, + }, + title: 'saved', + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + dateRange: { start: 1572469587644, end: 1572555987644 }, + savedObjectId: '11169110-fc22-11e9-8ca9-072f15ce2685', + show: true, + sort: { columnId: '@timestamp', sortDirection: Direction.desc }, + width: 1100, + version: 'WzM4LDFd', + id: '11169110-fc22-11e9-8ca9-072f15ce2685', + savedQueryId: 'my endgame timeline query', + }; + + expect( + convertTimelineAsInput(timelineModel, { + kind: 'absolute', + from: 1572469587644, + fromStr: undefined, + to: 1572555987644, + toStr: undefined, + }) + ).toEqual({ + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + }, + ], + dataProviders: [ + { + and: [ + { + enabled: true, + excluded: false, + id: + 'plain-column-renderer-data-provider-hosts-page-event_module-CQg7I24BHe9nqdOi_LYL-event_module-endgame', + kqlQuery: '', + name: 'event.module: endgame', + queryMatch: { + field: 'event.module', + operator: ':', + value: 'endgame', + }, + }, + ], + enabled: true, + excluded: false, + id: 'hosts-table-hostName-DESKTOP-QBBSCUT', + kqlQuery: '', + name: 'DESKTOP-QBBSCUT', + queryMatch: { + field: 'host.name', + operator: ':', + value: 'DESKTOP-QBBSCUT', + }, + }, + ], + dateRange: { + end: 1572555987644, + start: 1572469587644, + }, + description: '', + filters: [ + { + exists: null, + match_all: null, + meta: { + alias: null, + disabled: false, + field: null, + key: 'event.category', + negate: false, + params: '{"query":"file"}', + type: 'phrase', + value: null, + }, + missing: null, + query: '{"match_phrase":{"event.category":"file"}}', + range: null, + script: null, + }, + { + exists: '{"field":"@timestamp"}', + match_all: null, + meta: { + alias: null, + disabled: false, + field: null, + key: '@timestamp', + negate: false, + params: null, + type: 'exists', + value: 'exists', + }, + missing: null, + query: null, + range: null, + script: null, + }, + ], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + expression: 'endgame.user_name : "zeus" ', + kind: 'kuery', + }, + serializedQuery: + '{"bool":{"should":[{"match_phrase":{"endgame.user_name":"zeus"}}],"minimum_should_match":1}}', + }, + }, + savedQueryId: 'my endgame timeline query', + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + title: 'saved', + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts index 6957db5578af57..a9cf7cff812adc 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts @@ -4,7 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, has, merge as mergeObject, set, omit } from 'lodash/fp'; +import { + get, + has, + merge as mergeObject, + set, + omit, + isObject, + toString as fpToString, +} from 'lodash/fp'; import { Action } from 'redux'; import { Epic } from 'redux-observable'; import { from, Observable, empty, merge } from 'rxjs'; @@ -20,6 +28,7 @@ import { takeUntil, } from 'rxjs/operators'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header'; import { persistTimelineMutation } from '../../containers/timeline/persist.gql_query'; import { @@ -52,6 +61,8 @@ import { updateTimeline, updateTitle, updateAutoSaveMsg, + setFilters, + setSavedQueryId, startTimelineSaving, endTimelineSaving, createTimeline, @@ -81,6 +92,8 @@ const timelineActionsType = [ dataProviderEdited.type, removeColumn.type, removeProvider.type, + setFilters.type, + setSavedQueryId.type, updateColumns.type, updateDataProviderEnabled.type, updateDataProviderExcluded.type, @@ -235,14 +248,16 @@ const timelineInput: TimelineInput = { columns: null, dataProviders: null, description: null, + filters: null, kqlMode: null, kqlQuery: null, title: null, dateRange: null, + savedQueryId: null, sort: null, }; -const convertTimelineAsInput = ( +export const convertTimelineAsInput = ( timeline: TimelineModel, timelineTimeRange: TimeRange ): TimelineInput => @@ -258,6 +273,65 @@ const convertTimelineAsInput = ( get(key, timeline).map((col: ColumnHeader) => omit(['width', '__typename'], col)), acc ); + } else if (key === 'filters' && get(key, timeline) != null) { + const filters = get(key, timeline); + return set( + key, + filters != null + ? filters.map((myFilter: esFilters.Filter) => { + const basicFilter = omit(['$state'], myFilter); + return { + ...basicFilter, + meta: { + ...basicFilter.meta, + field: + (esFilters.isMatchAllFilter(basicFilter) || + esFilters.isPhraseFilter(basicFilter) || + esFilters.isPhrasesFilter(basicFilter) || + esFilters.isRangeFilter(basicFilter)) && + basicFilter.meta.field != null + ? convertToString(basicFilter.meta.field) + : null, + value: + basicFilter.meta.value != null + ? convertToString(basicFilter.meta.value) + : null, + params: + basicFilter.meta.params != null + ? convertToString(basicFilter.meta.params) + : null, + }, + ...(esFilters.isMatchAllFilter(basicFilter) + ? { + match_all: convertToString( + (basicFilter as esFilters.MatchAllFilter).match_all + ), + } + : { match_all: null }), + ...(esFilters.isMissingFilter(basicFilter) && basicFilter.missing != null + ? { missing: convertToString(basicFilter.missing) } + : { missing: null }), + ...(esFilters.isExistsFilter(basicFilter) && basicFilter.exists != null + ? { exists: convertToString(basicFilter.exists) } + : { exists: null }), + ...((esFilters.isQueryStringFilter(basicFilter) || + get('query', basicFilter) != null) && + basicFilter.query != null + ? { query: convertToString(basicFilter.query) } + : { query: null }), + ...(esFilters.isRangeFilter(basicFilter) && basicFilter.range != null + ? { range: convertToString(basicFilter.range) } + : { range: null }), + ...(esFilters.isRangeFilter(basicFilter) && + basicFilter.script != + null /* TODO remove it when PR50713 is merged || esFilters.isPhraseFilter(basicFilter) */ + ? { script: convertToString(basicFilter.script) } + : { script: null }), + }; + }) + : [], + acc + ); } return set(key, get(key, timeline), acc); } @@ -271,3 +345,14 @@ const omitTypenameInTimeline = ( oldTimeline: TimelineModel, newTimeline: TimelineResult ): TimelineModel => JSON.parse(JSON.stringify(mergeObject(oldTimeline, newTimeline)), omitTypename); + +const convertToString = (obj: unknown) => { + try { + if (isObject(obj)) { + return JSON.stringify(obj); + } + return fpToString(obj); + } catch { + return ''; + } +}; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts index eee467cd9d6d44..16ae53ade79694 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts @@ -3,8 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { getOr, omit, uniq, isEmpty, isEqualWith } from 'lodash/fp'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header'; import { getColumnWidthFromType } from '../../components/timeline/body/helpers'; import { Sort } from '../../components/timeline/body/sort'; @@ -1135,3 +1137,43 @@ export const updateHighlightedDropAndProvider = ({ }, }; }; + +interface UpdateSavedQueryParams { + id: string; + savedQueryId: string | null; + timelineById: TimelineById; +} + +export const updateSavedQuery = ({ + id, + savedQueryId, + timelineById, +}: UpdateSavedQueryParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + savedQueryId, + }, + }; +}; + +interface UpdateFiltersParams { + id: string; + filters: esFilters.Filter[]; + timelineById: TimelineById; +} + +export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + filters, + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts index 3b10314f72531c..405564a4b5b0d6 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { esFilters } from '../../../../../../../src/plugins/data/public'; import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header'; import { DataProvider } from '../../components/timeline/data_providers/data_provider'; import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/helpers'; @@ -25,6 +26,7 @@ export interface TimelineModel { description: string; /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ eventIdToNoteIds: Record; + filters?: esFilters.Filter[]; /** The chronological history of actions related to this timeline */ historyIds: string[]; /** The chronological history of actions related to this timeline */ @@ -59,6 +61,7 @@ export interface TimelineModel { start: number; end: number; }; + savedQueryId?: string | null; /** When true, show the timeline flyover */ show: boolean; /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ @@ -77,6 +80,7 @@ export const timelineDefaults: Readonly ({ ...state, - timelineById: applyKqlFilterQueryDraft({ id, filterQuery, timelineById: state.timelineById }), + timelineById: applyKqlFilterQueryDraft({ + id, + filterQuery, + timelineById: state.timelineById, + }), })) .case(setKqlFilterQueryDraft, (state, { id, filterQueryDraft }) => ({ ...state, @@ -361,4 +369,20 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, showCallOutUnauthorizedMsg: true, })) + .case(setSavedQueryId, (state, { id, savedQueryId }) => ({ + ...state, + timelineById: updateSavedQuery({ + id, + savedQueryId, + timelineById: state.timelineById, + }), + })) + .case(setFilters, (state, { id, filters }) => ({ + ...state, + timelineById: updateFilters({ + id, + filters, + timelineById: state.timelineById, + }), + })) .build(); diff --git a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx index de6fedb687d97a..b70a5432e47f84 100644 --- a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx @@ -14,12 +14,6 @@ mockDispatch.mockImplementation(fn => fn); const applyTimelineKqlMock: jest.Mock = (dispatchApplyTimelineFilterQuery as unknown) as jest.Mock; -jest.mock('../../store/hosts/actions', () => ({ - applyHostsFilterQuery: jest.fn(), -})); -jest.mock('../../store/network/actions', () => ({ - applyNetworkFilterQuery: jest.fn(), -})); jest.mock('../../store/timeline/actions', () => ({ applyKqlFilterQuery: jest.fn(), })); @@ -36,7 +30,6 @@ describe('#useUpdateKql', () => { kueryFilterQuery: { expression: '', kind: 'kuery' }, kueryFilterQueryDraft: { expression: 'host.name: "myLove"', kind: 'kuery' }, storeType: 'timelineType', - type: null, timelineId: 'myTimelineId', })(mockDispatch); expect(applyTimelineKqlMock).toHaveBeenCalledWith({ diff --git a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx index 1a4ca656ba0fee..b5843d149d24de 100644 --- a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx +++ b/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx @@ -18,7 +18,6 @@ interface UseUpdateKqlProps { kueryFilterQuery: KueryFilterQuery | null; kueryFilterQueryDraft: KueryFilterQuery | null; storeType: 'timelineType'; - type: null; timelineId?: string; } @@ -28,7 +27,6 @@ export const useUpdateKql = ({ kueryFilterQueryDraft, storeType, timelineId, - type, }: UseUpdateKqlProps): RefetchKql => { const updateKql: RefetchKql = (dispatch: Dispatch) => { if (kueryFilterQueryDraft != null && !isEqual(kueryFilterQuery, kueryFilterQueryDraft)) { @@ -37,10 +35,7 @@ export const useUpdateKql = ({ dispatchApplyTimelineFilterQuery({ id: timelineId, filterQuery: { - kuery: { - kind: 'kuery', - expression: kueryFilterQueryDraft.expression, - }, + kuery: kueryFilterQueryDraft, serializedQuery: convertKueryToElasticSearchQuery( kueryFilterQueryDraft.expression, indexPattern @@ -49,7 +44,6 @@ export const useUpdateKql = ({ }) ); } - return true; } return false; diff --git a/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx b/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx new file mode 100644 index 00000000000000..f1e4cf34113980 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect } from 'react'; +import { + SavedQueryService, + createSavedQueryService, +} from '../../../../../../../src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service'; + +import { useKibanaCore } from '../../lib/compose/kibana_core'; + +export const useSavedQueryServices = () => { + const core = useKibanaCore(); + const [savedQueryService, setSavedQueryService] = useState( + createSavedQueryService(core.savedObjects.client) + ); + + useEffect(() => { + setSavedQueryService(createSavedQueryService(core.savedObjects.client)); + }, [core.savedObjects.client]); + return savedQueryService; +}; diff --git a/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts index a417ecb82b2d23..f05c26de7f75c6 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts @@ -49,6 +49,20 @@ const sortTimeline = ` sortDirection: String `; +const filtersMetaTimeline = ` + alias: String + controlledBy: String + disabled: Boolean + field: String + formattedValue: String + index: String + key: String + negate: Boolean + params: String + type: String + value: String +`; + export const timelineSchema = gql` ############### #### INPUT #### @@ -97,14 +111,30 @@ export const timelineSchema = gql` ${sortTimeline} } + input FilterMetaTimelineInput { + ${filtersMetaTimeline} + } + + input FilterTimelineInput { + exists: String + meta: FilterMetaTimelineInput + match_all: String + missing: String + query: String + range: String + script: String + } + input TimelineInput { columns: [ColumnHeaderInput!] dataProviders: [DataProviderInput!] description: String + filters: [FilterTimelineInput!] kqlMode: String kqlQuery: SerializedFilterQueryInput title: String dateRange: DateRangePickerInput + savedQueryId: String sort: SortTimelineInput } @@ -171,24 +201,40 @@ export const timelineSchema = gql` ${sortTimeline} } + type FilterMetaTimelineResult { + ${filtersMetaTimeline} + } + + type FilterTimelineResult { + exists: String + meta: FilterMetaTimelineResult + match_all: String + missing: String + query: String + range: String + script: String + } + type TimelineResult { - savedObjectId: String! columns: [ColumnHeaderResult!] + created: Float + createdBy: String dataProviders: [DataProviderResult!] dateRange: DateRangePickerResult description: String eventIdToNoteIds: [NoteResult!] favorite: [FavoriteTimelineResult!] + filters: [FilterTimelineResult!] kqlMode: String kqlQuery: SerializedFilterQueryResult notes: [NoteResult!] noteIds: [String!] pinnedEventIds: [String!] pinnedEventsSaveObject: [PinnedEvent!] - title: String + savedQueryId: String + savedObjectId: String! sort: SortTimelineResult - created: Float - createdBy: String + title: String updated: Float updatedBy: String version: String! diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index cf7ce3ad02fa18..d6a4d204124a1c 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -124,6 +124,8 @@ export interface TimelineInput { description?: Maybe; + filters?: Maybe; + kqlMode?: Maybe; kqlQuery?: Maybe; @@ -132,6 +134,8 @@ export interface TimelineInput { dateRange?: Maybe; + savedQueryId?: Maybe; + sort?: Maybe; } @@ -187,6 +191,46 @@ export interface QueryMatchInput { operator?: Maybe; } +export interface FilterTimelineInput { + exists?: Maybe; + + meta?: Maybe; + + match_all?: Maybe; + + missing?: Maybe; + + query?: Maybe; + + range?: Maybe; + + script?: Maybe; +} + +export interface FilterMetaTimelineInput { + alias?: Maybe; + + controlledBy?: Maybe; + + disabled?: Maybe; + + field?: Maybe; + + formattedValue?: Maybe; + + index?: Maybe; + + key?: Maybe; + + negate?: Maybe; + + params?: Maybe; + + type?: Maybe; + + value?: Maybe; +} + export interface SerializedFilterQueryInput { filterQuery?: Maybe; } @@ -1762,10 +1806,12 @@ export interface SayMyName { } export interface TimelineResult { - savedObjectId: string; - columns?: Maybe; + created?: Maybe; + + createdBy?: Maybe; + dataProviders?: Maybe; dateRange?: Maybe; @@ -1776,6 +1822,8 @@ export interface TimelineResult { favorite?: Maybe; + filters?: Maybe; + kqlMode?: Maybe; kqlQuery?: Maybe; @@ -1788,13 +1836,13 @@ export interface TimelineResult { pinnedEventsSaveObject?: Maybe; - title?: Maybe; + savedQueryId?: Maybe; - sort?: Maybe; + savedObjectId: string; - created?: Maybe; + sort?: Maybe; - createdBy?: Maybe; + title?: Maybe; updated?: Maybe; @@ -1869,6 +1917,46 @@ export interface FavoriteTimelineResult { favoriteDate?: Maybe; } +export interface FilterTimelineResult { + exists?: Maybe; + + meta?: Maybe; + + match_all?: Maybe; + + missing?: Maybe; + + query?: Maybe; + + range?: Maybe; + + script?: Maybe; +} + +export interface FilterMetaTimelineResult { + alias?: Maybe; + + controlledBy?: Maybe; + + disabled?: Maybe; + + field?: Maybe; + + formattedValue?: Maybe; + + index?: Maybe; + + key?: Maybe; + + negate?: Maybe; + + params?: Maybe; + + type?: Maybe; + + value?: Maybe; +} + export interface SerializedFilterQueryResult { filterQuery?: Maybe; } @@ -7469,10 +7557,12 @@ export namespace SayMyNameResolvers { export namespace TimelineResultResolvers { export interface Resolvers { - savedObjectId?: SavedObjectIdResolver; - columns?: ColumnsResolver, TypeParent, TContext>; + created?: CreatedResolver, TypeParent, TContext>; + + createdBy?: CreatedByResolver, TypeParent, TContext>; + dataProviders?: DataProvidersResolver, TypeParent, TContext>; dateRange?: DateRangeResolver, TypeParent, TContext>; @@ -7483,6 +7573,8 @@ export namespace TimelineResultResolvers { favorite?: FavoriteResolver, TypeParent, TContext>; + filters?: FiltersResolver, TypeParent, TContext>; + kqlMode?: KqlModeResolver, TypeParent, TContext>; kqlQuery?: KqlQueryResolver, TypeParent, TContext>; @@ -7499,13 +7591,13 @@ export namespace TimelineResultResolvers { TContext >; - title?: TitleResolver, TypeParent, TContext>; + savedQueryId?: SavedQueryIdResolver, TypeParent, TContext>; - sort?: SortResolver, TypeParent, TContext>; + savedObjectId?: SavedObjectIdResolver; - created?: CreatedResolver, TypeParent, TContext>; + sort?: SortResolver, TypeParent, TContext>; - createdBy?: CreatedByResolver, TypeParent, TContext>; + title?: TitleResolver, TypeParent, TContext>; updated?: UpdatedResolver, TypeParent, TContext>; @@ -7514,13 +7606,18 @@ export namespace TimelineResultResolvers { version?: VersionResolver; } - export type SavedObjectIdResolver< - R = string, + export type ColumnsResolver< + R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type ColumnsResolver< - R = Maybe, + export type CreatedResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; + export type CreatedByResolver< + R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; @@ -7549,6 +7646,11 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver; + export type FiltersResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; export type KqlModeResolver< R = Maybe, Parent = TimelineResult, @@ -7579,22 +7681,22 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type TitleResolver< + export type SavedQueryIdResolver< R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type SortResolver< - R = Maybe, + export type SavedObjectIdResolver< + R = string, Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type CreatedResolver< - R = Maybe, + export type SortResolver< + R = Maybe, Parent = TimelineResult, TContext = SiemContext > = Resolver; - export type CreatedByResolver< + export type TitleResolver< R = Maybe, Parent = TimelineResult, TContext = SiemContext @@ -7837,6 +7939,142 @@ export namespace FavoriteTimelineResultResolvers { > = Resolver; } +export namespace FilterTimelineResultResolvers { + export interface Resolvers { + exists?: ExistsResolver, TypeParent, TContext>; + + meta?: MetaResolver, TypeParent, TContext>; + + match_all?: MatchAllResolver, TypeParent, TContext>; + + missing?: MissingResolver, TypeParent, TContext>; + + query?: QueryResolver, TypeParent, TContext>; + + range?: RangeResolver, TypeParent, TContext>; + + script?: ScriptResolver, TypeParent, TContext>; + } + + export type ExistsResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type MetaResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type MatchAllResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type MissingResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type QueryResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type RangeResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; + export type ScriptResolver< + R = Maybe, + Parent = FilterTimelineResult, + TContext = SiemContext + > = Resolver; +} + +export namespace FilterMetaTimelineResultResolvers { + export interface Resolvers { + alias?: AliasResolver, TypeParent, TContext>; + + controlledBy?: ControlledByResolver, TypeParent, TContext>; + + disabled?: DisabledResolver, TypeParent, TContext>; + + field?: FieldResolver, TypeParent, TContext>; + + formattedValue?: FormattedValueResolver, TypeParent, TContext>; + + index?: IndexResolver, TypeParent, TContext>; + + key?: KeyResolver, TypeParent, TContext>; + + negate?: NegateResolver, TypeParent, TContext>; + + params?: ParamsResolver, TypeParent, TContext>; + + type?: TypeResolver, TypeParent, TContext>; + + value?: ValueResolver, TypeParent, TContext>; + } + + export type AliasResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type ControlledByResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type DisabledResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type FieldResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type FormattedValueResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type IndexResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type KeyResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type NegateResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type ParamsResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type TypeResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; + export type ValueResolver< + R = Maybe, + Parent = FilterMetaTimelineResult, + TContext = SiemContext + > = Resolver; +} + export namespace SerializedFilterQueryResultResolvers { export interface Resolvers { filterQuery?: FilterQueryResolver, TypeParent, TContext>; @@ -8484,6 +8722,8 @@ export type IResolvers = { QueryMatchResult?: QueryMatchResultResolvers.Resolvers; DateRangePickerResult?: DateRangePickerResultResolvers.Resolvers; FavoriteTimelineResult?: FavoriteTimelineResultResolvers.Resolvers; + FilterTimelineResult?: FilterTimelineResultResolvers.Resolvers; + FilterMetaTimelineResult?: FilterMetaTimelineResultResolvers.Resolvers; SerializedFilterQueryResult?: SerializedFilterQueryResultResolvers.Resolvers; SerializedKueryQueryResult?: SerializedKueryQueryResultResolvers.Resolvers; KueryFilterQueryResult?: KueryFilterQueryResultResolvers.Resolvers; diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts index f9175eb8ffb904..8c7275a86911bf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object_mappings.ts @@ -146,6 +146,65 @@ export const timelineSavedObjectMappings: { }, }, }, + filters: { + properties: { + meta: { + properties: { + alias: { + type: 'text', + }, + controlledBy: { + type: 'text', + }, + disabled: { + type: 'boolean', + }, + field: { + type: 'text', + }, + formattedValue: { + type: 'text', + }, + index: { + type: 'keyword', + }, + key: { + type: 'keyword', + }, + negate: { + type: 'boolean', + }, + params: { + type: 'text', + }, + type: { + type: 'keyword', + }, + value: { + type: 'text', + }, + }, + }, + exists: { + type: 'text', + }, + match_all: { + type: 'text', + }, + missing: { + type: 'text', + }, + query: { + type: 'text', + }, + range: { + type: 'text', + }, + script: { + type: 'text', + }, + }, + }, kqlMode: { type: 'keyword', }, @@ -183,6 +242,9 @@ export const timelineSavedObjectMappings: { }, }, }, + savedQueryId: { + type: 'keyword', + }, sort: { properties: { columnId: { diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts index 0ffdf73f4c74f5..72e5cd50af394e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/types.ts @@ -59,6 +59,33 @@ const SavedDataProviderRuntimeType = runtimeTypes.partial({ and: unionWithNullType(runtimeTypes.array(SavedDataProviderQueryMatchRuntimeType)), }); +/* + * Filters Types + */ +const SavedFilterMetaRuntimeType = runtimeTypes.partial({ + alias: unionWithNullType(runtimeTypes.string), + controlledBy: unionWithNullType(runtimeTypes.string), + disabled: unionWithNullType(runtimeTypes.boolean), + field: unionWithNullType(runtimeTypes.string), + formattedValue: unionWithNullType(runtimeTypes.string), + index: unionWithNullType(runtimeTypes.string), + key: unionWithNullType(runtimeTypes.string), + negate: unionWithNullType(runtimeTypes.boolean), + params: unionWithNullType(runtimeTypes.string), + type: unionWithNullType(runtimeTypes.string), + value: unionWithNullType(runtimeTypes.string), +}); + +const SavedFilterRuntimeType = runtimeTypes.partial({ + exists: unionWithNullType(runtimeTypes.string), + meta: unionWithNullType(SavedFilterMetaRuntimeType), + match_all: unionWithNullType(runtimeTypes.string), + missing: unionWithNullType(runtimeTypes.string), + query: unionWithNullType(runtimeTypes.string), + range: unionWithNullType(runtimeTypes.string), + script: unionWithNullType(runtimeTypes.string), +}); + /* * kqlQuery -> filterQuery Types */ @@ -110,10 +137,12 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({ dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), description: unionWithNullType(runtimeTypes.string), favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), + filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), kqlMode: unionWithNullType(runtimeTypes.string), kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType), title: unionWithNullType(runtimeTypes.string), dateRange: unionWithNullType(SavedDateRangePickerRuntimeType), + savedQueryId: unionWithNullType(runtimeTypes.string), sort: unionWithNullType(SavedSortRuntimeType), created: unionWithNullType(runtimeTypes.number), createdBy: unionWithNullType(runtimeTypes.string), From eb0141d6609185dfcb43750d13d9b8bb51f5e5a7 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Fri, 15 Nov 2019 09:06:22 +0100 Subject: [PATCH 13/28] [SIEM] Fix IE11 timeline drag and drop issue (#50528) --- .../page/add_filter_to_global_search_bar/index.tsx | 6 +++--- .../components/timeline/data_providers/providers.tsx | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx index 6e1e6545e5534c..012d8322fa3293 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx @@ -33,7 +33,7 @@ export const AddFilterToGlobalSearchBar = React.memo( return ( + @@ -51,11 +51,11 @@ export const HoverActionsContainer = styled(EuiPanel)` align-items: center; display: flex; flex-direction: row; - height: 25px; + height: 34px; justify-content: center; left: 5px; position: absolute; top: -10px; - width: 30px; + width: 34px; cursor: pointer; `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx index ac0f9bbe9c8782..112962367cd362 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx @@ -61,6 +61,14 @@ PanelProviders.displayName = 'PanelProviders'; const PanelProvidersGroupContainer = styled(EuiFlexGroup)` position: relative; flex-grow: unset; + + .euiFlexItem { + flex: 1 0 auto; + } + + .euiFlexItem--flexGrowZero { + flex: 0 0 auto; + } `; PanelProvidersGroupContainer.displayName = 'PanelProvidersGroupContainer'; @@ -68,6 +76,7 @@ PanelProvidersGroupContainer.displayName = 'PanelProvidersGroupContainer'; /** A row of data providers in the timeline drop zone */ const PanelProviderGroupContainer = styled(EuiFlexGroup)` height: ${ROW_OF_DATA_PROVIDERS_HEIGHT}px; + min-height: ${ROW_OF_DATA_PROVIDERS_HEIGHT}px; margin: 5px 0px; `; From 38899f0879515bf96d9dbfe18cdc288e880fc27a Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Fri, 15 Nov 2019 09:57:52 +0100 Subject: [PATCH 14/28] fixes conditional links tests (#50642) --- .../ml_conditional_links.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts index 5485942c0f624f..a03ff0c1845f85 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts @@ -104,7 +104,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkSingleIpNullKqlQuery); cy.url().should( 'include', - '/app/siem#/network/ip/127.0.0.1?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/siem#/network/ip/127.0.0.1?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' ); }); @@ -112,7 +112,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkSingleIpKqlQuery); cy.url().should( 'include', - "/app/siem#/network/ip/127.0.0.1?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + "/app/siem#/network/ip/127.0.0.1?_g=()&query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" ); }); @@ -120,7 +120,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkMultipleIpNullKqlQuery); cy.url().should( 'include', - "/app/siem#/network?query=(language:kuery,query:'((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + "app/siem#/network/flows?_g=()&query=(language:kuery,query:'((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999))" ); }); @@ -128,7 +128,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkMultipleIpKqlQuery); cy.url().should( 'include', - "/app/siem#/network?query=(language:kuery,query:'((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + "/app/siem#/network/flows?_g=()&query=(language:kuery,query:'((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" ); }); @@ -136,7 +136,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkNullKqlQuery); cy.url().should( 'include', - '/app/siem#/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/siem#/network/flows?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' ); }); @@ -144,7 +144,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkKqlQuery); cy.url().should( 'include', - "/app/siem#/network?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + "/app/siem#/network/flows?_g=()&query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" ); }); From c3d0349fb04717282c5e5452fbc7404f54d50596 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 15 Nov 2019 10:29:38 +0100 Subject: [PATCH 15/28] fix: hide 'edit' button for mobile for dashboards (#50639) --- .../navigation/public/top_nav_menu/top_nav_menu_item.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx index d4d1c159906aa9..4d3b72bae64115 100644 --- a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu_item.tsx @@ -45,6 +45,7 @@ export function TopNavMenuItem(props: TopNavMenuData) { isDisabled={isDisabled()} onClick={handleClick} data-test-subj={props.testId} + className={props.className} > {capitalize(props.label || props.id!)} From 70d6220a41d96f25b0634f3962de6a1fa340eefb Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Fri, 15 Nov 2019 10:33:29 +0100 Subject: [PATCH 16/28] Upgrade to TypeScript 3.7.2 (#47188) * Update TS to 3.7.0-beta * Upgrade to TypeScript 3.7.2 * Upgrade cypress to 3.5.0 * Upgrade apollo-link-http * Update prettier * Fix expression types * Fix Lens test typings * Fix ML breakage * Fix APM breakage * Fix sinon stub typing * Fix beats management types * Fix WMSOptions types * Fix ui_filters in APM * APM: Explicitly type Setup to prevent hitting TS limits * Change file name to correct case * Add styleguide rule for optional chaining * Update typescript-eslint packages * Revert changes in ui filters routes that are no longer needed --- STYLEGUIDE.md | 1 + package.json | 8 +- packages/eslint-config-kibana/package.json | 4 +- packages/kbn-analytics/package.json | 2 +- packages/kbn-config-schema/package.json | 2 +- packages/kbn-dev-utils/package.json | 2 +- packages/kbn-elastic-idx/package.json | 2 +- packages/kbn-i18n/package.json | 2 +- packages/kbn-pm/package.json | 2 +- .../console/server/request.test.ts | 4 +- .../public/components/wms_options.tsx | 19 +-- src/plugins/expressions/public/types/index.ts | 2 - .../plugins/core_plugin_a/package.json | 2 +- .../plugins/core_plugin_b/package.json | 2 +- .../plugins/core_plugin_legacy/package.json | 2 +- .../kbn_tp_embeddable_explorer/package.json | 2 +- .../kbn_tp_sample_panel_action/package.json | 2 +- .../legacy/plugins/apm/cypress/package.json | 4 +- .../apm/server/lib/helpers/setup_request.ts | 24 +++- .../lib/metrics/transform_metrics_chart.ts | 7 +- .../public/utils/typed_react.ts | 4 +- .../indexpattern_plugin/datapanel.test.tsx | 4 +- .../data_frame_analytics/common/analytics.ts | 1 - .../csv/server/lib/__tests__/hit_iterator.ts | 2 +- x-pack/package.json | 6 +- x-pack/typings/global_fetch.d.ts | 9 ++ yarn.lock | 116 +++++++++++------- 27 files changed, 145 insertions(+), 92 deletions(-) create mode 100644 x-pack/typings/global_fetch.d.ts diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 5fd3ef5e8ff4bf..461d51a3e76e30 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -120,6 +120,7 @@ You should prefer modern language features in a lot of cases, e.g.: * Prefer arrow function over storing `this` (no `const self = this;`) * Prefer template strings over string concatenation * Prefer the spread operator for copying arrays (`[...arr]`) over `arr.slice()` +* Use optional chaining (`?.`) and nullish Coalescing (`??`) over `lodash.get` (and similar utilities) ### Avoid mutability and state diff --git a/package.json b/package.json index 8fa9bf1847eb81..c0e2acdb578c49 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "**/@types/react": "16.8.3", "**/@types/hapi": "^17.0.18", "**/@types/angular": "^1.6.56", - "**/typescript": "3.5.3", + "**/typescript": "3.7.2", "**/graphql-toolkit/lodash": "^4.17.13", "**/isomorphic-git/**/base64-js": "^1.2.1", "**/image-diff/gm/debug": "^2.6.9" @@ -346,8 +346,8 @@ "@types/uuid": "^3.4.4", "@types/vinyl-fs": "^2.4.11", "@types/zen-observable": "^0.8.0", - "@typescript-eslint/eslint-plugin": "^2.5.0", - "@typescript-eslint/parser": "^2.5.0", + "@typescript-eslint/eslint-plugin": "^2.7.0", + "@typescript-eslint/parser": "^2.7.0", "angular-mocks": "^1.7.8", "archiver": "^3.1.1", "axe-core": "^3.3.2", @@ -444,7 +444,7 @@ "supertest": "^3.1.0", "supertest-as-promised": "^4.0.2", "tree-kill": "^1.2.1", - "typescript": "3.5.3", + "typescript": "3.7.2", "typings-tester": "^0.3.2", "vinyl-fs": "^3.0.3", "xml2js": "^0.4.22", diff --git a/packages/eslint-config-kibana/package.json b/packages/eslint-config-kibana/package.json index b5079a49c8385d..c67629f058d5ae 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.5.0", - "@typescript-eslint/parser": "^2.5.0", + "@typescript-eslint/eslint-plugin": "^2.7.0", + "@typescript-eslint/parser": "^2.7.0", "babel-eslint": "^10.0.3", "eslint": "^6.5.1", "eslint-plugin-babel": "^5.3.0", diff --git a/packages/kbn-analytics/package.json b/packages/kbn-analytics/package.json index b0ac86b465a621..f59fbf4720835e 100644 --- a/packages/kbn-analytics/package.json +++ b/packages/kbn-analytics/package.json @@ -17,6 +17,6 @@ "@babel/cli": "7.5.5", "@kbn/dev-utils": "1.0.0", "@kbn/babel-preset": "1.0.0", - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/packages/kbn-config-schema/package.json b/packages/kbn-config-schema/package.json index 4880fb4ebfdeeb..71c0ae4bff1f9d 100644 --- a/packages/kbn-config-schema/package.json +++ b/packages/kbn-config-schema/package.json @@ -10,7 +10,7 @@ "kbn:bootstrap": "yarn build" }, "devDependencies": { - "typescript": "3.5.3" + "typescript": "3.7.2" }, "peerDependencies": { "joi": "^13.5.2", diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index e8781f6d901d92..09753afeb120f4 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -21,7 +21,7 @@ "tslib": "^1.9.3" }, "devDependencies": { - "typescript": "3.5.3", + "typescript": "3.7.2", "@kbn/expect": "1.0.0", "chance": "1.0.18" } diff --git a/packages/kbn-elastic-idx/package.json b/packages/kbn-elastic-idx/package.json index abfaea75357dd8..9532983942d6b9 100644 --- a/packages/kbn-elastic-idx/package.json +++ b/packages/kbn-elastic-idx/package.json @@ -20,7 +20,7 @@ "@babel/core": "^7.5.5", "@babel/plugin-transform-async-to-generator": "^7.5.0", "jest": "^24.9.0", - "typescript": "3.5.3" + "typescript": "3.7.2" }, "jest": { "testEnvironment": "node" diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 8a88626bffbe85..3e25ceb8714df0 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -21,7 +21,7 @@ "del": "^5.1.0", "getopts": "^2.2.4", "supports-color": "^7.0.0", - "typescript": "3.5.3" + "typescript": "3.7.2" }, "dependencies": { "intl-format-cache": "^2.1.0", diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index ac46dd02757cf9..2f9b177be65325 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -58,7 +58,7 @@ "strip-ansi": "^4.0.0", "strong-log-transformer": "^2.1.0", "tempy": "^0.3.0", - "typescript": "3.5.3", + "typescript": "3.7.2", "unlazy-loader": "^0.1.3", "webpack": "^4.41.0", "webpack-cli": "^3.3.9", diff --git a/src/legacy/core_plugins/console/server/request.test.ts b/src/legacy/core_plugins/console/server/request.test.ts index 463649a0902954..d5504c0f3a3c26 100644 --- a/src/legacy/core_plugins/console/server/request.test.ts +++ b/src/legacy/core_plugins/console/server/request.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import http from 'http'; +import http, { ClientRequest } from 'http'; import * as sinon from 'sinon'; import { sendRequest } from './request'; import { URL } from 'url'; @@ -24,7 +24,7 @@ import { fail } from 'assert'; describe(`Console's send request`, () => { let sandbox: sinon.SinonSandbox; - let stub: sinon.SinonStub; + let stub: sinon.SinonStub, ClientRequest>; let fakeRequest: http.ClientRequest; beforeEach(() => { diff --git a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx b/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx index ef6b2eaea1e521..c5ccc3acba6104 100644 --- a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx @@ -23,23 +23,26 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { TmsLayer } from 'ui/vis/map/service_settings'; +import { Vis } from 'ui/vis'; +import { RegionMapVisParams } from '../../../region_map/public/types'; import { SelectOption, SwitchOption } from '../../../kbn_vislib_vis_types/public/components'; -import { RegionMapOptionsProps } from '../../../region_map/public/components/region_map_options'; import { WmsInternalOptions } from './wms_internal_options'; -import { TileMapOptionsProps } from './tile_map_options'; -import { TileMapVisParams } from '../types'; +import { WMSOptions, TileMapVisParams } from '../types'; + +interface Props { + stateParams: TileMapVisParams | RegionMapVisParams; + setValue: (title: 'wms', options: WMSOptions) => void; + vis: Vis; +} const mapLayerForOption = ({ id }: TmsLayer) => ({ text: id, value: id }); -function WmsOptions({ stateParams, setValue, vis }: TileMapOptionsProps | RegionMapOptionsProps) { +function WmsOptions({ stateParams, setValue, vis }: Props) { const { wms } = stateParams; const { tmsLayers } = vis.type.editorConfig.collections; const tmsLayerOptions = useMemo(() => tmsLayers.map(mapLayerForOption), [tmsLayers]); - const setWmsOption = ( - paramName: T, - value: TileMapVisParams['wms'][T] - ) => + const setWmsOption = (paramName: T, value: WMSOptions[T]) => setValue('wms', { ...wms, [paramName]: value, diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 87ef810682f60b..faceef4f90a6fe 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -21,8 +21,6 @@ import { ExpressionInterpret } from '../interpreter_provider'; import { TimeRange } from '../../../data/public'; import { Adapters } from '../../../inspector/public'; import { Query } from '../../../data/public'; -import { ExpressionAST } from '../../../expressions/public'; -import { ExpressionArgAST } from '../../../../plugins/expressions/public'; import { esFilters } from '../../../../plugins/data/public'; export { ArgumentType } from './arguments'; diff --git a/test/plugin_functional/plugins/core_plugin_a/package.json b/test/plugin_functional/plugins/core_plugin_a/package.json index 7dede34f3c0cf9..060ae49f43e8ae 100644 --- a/test/plugin_functional/plugins/core_plugin_a/package.json +++ b/test/plugin_functional/plugins/core_plugin_a/package.json @@ -12,6 +12,6 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/core_plugin_b/package.json b/test/plugin_functional/plugins/core_plugin_b/package.json index 18ff8cf7cc5c92..3eb878b9ed5dc4 100644 --- a/test/plugin_functional/plugins/core_plugin_b/package.json +++ b/test/plugin_functional/plugins/core_plugin_b/package.json @@ -12,6 +12,6 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/core_plugin_legacy/package.json b/test/plugin_functional/plugins/core_plugin_legacy/package.json index 2ae83b28f7e859..5f784c7b836a5f 100644 --- a/test/plugin_functional/plugins/core_plugin_legacy/package.json +++ b/test/plugin_functional/plugins/core_plugin_legacy/package.json @@ -12,6 +12,6 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 196e64af399850..9df9352f76fc27 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -17,6 +17,6 @@ }, "devDependencies": { "@kbn/plugin-helpers": "9.0.2", - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index 33e60128d08065..054276b6209078 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -17,6 +17,6 @@ }, "devDependencies": { "@kbn/plugin-helpers": "9.0.2", - "typescript": "3.5.3" + "typescript": "3.7.2" } } diff --git a/x-pack/legacy/plugins/apm/cypress/package.json b/x-pack/legacy/plugins/apm/cypress/package.json index 98dcd495b8594c..ef8955fcbd1b08 100644 --- a/x-pack/legacy/plugins/apm/cypress/package.json +++ b/x-pack/legacy/plugins/apm/cypress/package.json @@ -11,11 +11,11 @@ "@cypress/snapshot": "^2.1.3", "@cypress/webpack-preprocessor": "^4.1.0", "@types/js-yaml": "^3.12.1", - "cypress": "^3.4.1", + "cypress": "^3.5.0", "js-yaml": "^3.13.1", "p-limit": "^2.2.1", "ts-loader": "^6.1.0", - "typescript": "^3.6.3", + "typescript": "3.7.2", "webpack": "^4.40.2" } } diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index dcc034287863af..ab0f47eb04d628 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -7,10 +7,15 @@ import { Legacy } from 'kibana'; import { Server } from 'hapi'; import moment from 'moment'; +import { KibanaConfig } from 'src/legacy/server/kbn_server'; import { getESClient } from './es_client'; import { getUiFiltersES } from './convert_ui_filters/get_ui_filters_es'; -import { PromiseReturnType } from '../../../typings/common'; -import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; +import { + getApmIndices, + ApmIndicesConfig +} from '../settings/apm_indices/get_apm_indices'; +import { ESFilter } from '../../../typings/elasticsearch'; +import { ESClient } from './es_client'; function decodeUiFilters(server: Server, uiFiltersEncoded?: string) { if (!uiFiltersEncoded) { @@ -26,9 +31,20 @@ export interface APMRequestQuery { end?: string; uiFilters?: string; } +// Explicitly type Setup to prevent TS initialization errors +// https://github.com/microsoft/TypeScript/issues/34933 -export type Setup = PromiseReturnType; -export async function setupRequest(req: Legacy.Request) { +export interface Setup { + start: number; + end: number; + uiFiltersES: ESFilter[]; + client: ESClient; + internalClient: ESClient; + config: KibanaConfig; + indices: ApmIndicesConfig; +} + +export async function setupRequest(req: Legacy.Request): Promise { const query = (req.query as unknown) as APMRequestQuery; const { server } = req; const savedObjectsClient = server.savedObjects.getScopedSavedObjectsClient( diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index 594a0d35ed176c..3764f18a6d692f 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -48,9 +48,10 @@ type GenericMetricsRequest = Overwrite< } >; -export function transformDataToMetricsChart< - TRequest extends GenericMetricsRequest ->(result: ESSearchResponse, chartBase: ChartBase) { +export function transformDataToMetricsChart( + result: ESSearchResponse, + chartBase: ChartBase +) { const { aggregations, hits } = result; const timeseriesData = idx(aggregations, _ => _.timeseriesData); diff --git a/x-pack/legacy/plugins/beats_management/public/utils/typed_react.ts b/x-pack/legacy/plugins/beats_management/public/utils/typed_react.ts index 5557befa9d7e5d..dbc0894ab80710 100644 --- a/x-pack/legacy/plugins/beats_management/public/utils/typed_react.ts +++ b/x-pack/legacy/plugins/beats_management/public/utils/typed_react.ts @@ -43,7 +43,9 @@ export const asChildFunctionRenderer = ( } public render() { - return this.props.children(this.getRendererArgs()); + return (this.props.children as ChildFunctionRendererProps['children'])( + this.getRendererArgs() + ); } private getRendererArgs = () => diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx index 11294bedbfff0f..affb1accbbef46 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx @@ -329,10 +329,10 @@ describe('IndexPattern Data Panel', () => { act(() => { ((inst.setProps as unknown) as (props: unknown) => {})({ ...props, - ...(propChanges || {}), + ...((propChanges as object) || {}), state: { ...props.state, - ...(stateChanges || {}), + ...((stateChanges as object) || {}), }, }); inst.update(); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts index 385d50215cd21d..04dff6e0b4dc55 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts @@ -11,7 +11,6 @@ import { Subscription } from 'rxjs'; import { idx } from '@kbn/elastic-idx'; import { ml } from '../../services/ml_api_service'; import { getErrorMessage } from '../pages/analytics_management/hooks/use_create_analytics_form'; -import { RegressionEvaluateResponse } from '../common'; export type IndexName = string; export type IndexPattern = string; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/hit_iterator.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/hit_iterator.ts index c439c2bbf60ebe..20e373a4168af9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/hit_iterator.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/hit_iterator.ts @@ -22,7 +22,7 @@ const mockCallEndpoint = sinon.stub(); const mockSearchRequest = {}; const mockConfig: ScrollConfig = { duration: '2s', size: 123 }; let realCancellationToken = new CancellationToken(); -let isCancelledStub: sinon.SinonStub; +let isCancelledStub: sinon.SinonStub<[], boolean>; describe('hitIterator', function() { beforeEach(() => { diff --git a/x-pack/package.json b/x-pack/package.json index 1c86147f1bc32f..fabfce7a60c3bd 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -121,7 +121,7 @@ "cheerio": "0.22.0", "commander": "3.0.0", "copy-webpack-plugin": "^5.0.4", - "cypress": "^3.4.1", + "cypress": "^3.5.5", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.14.0", "enzyme-adapter-utils": "^1.12.0", @@ -171,7 +171,7 @@ "tmp": "0.1.0", "tree-kill": "^1.2.1", "ts-loader": "^6.0.4", - "typescript": "3.5.3", + "typescript": "3.7.2", "vinyl-fs": "^3.0.3", "whatwg-fetch": "^3.0.0", "xml-crypto": "^1.4.0", @@ -209,7 +209,7 @@ "apollo-client": "^2.3.8", "apollo-link": "^1.2.3", "apollo-link-error": "^1.1.7", - "apollo-link-http": "^1.5.4", + "apollo-link-http": "^1.5.16", "apollo-link-schema": "^1.1.0", "apollo-link-state": "^0.4.1", "apollo-server-errors": "^2.0.2", diff --git a/x-pack/typings/global_fetch.d.ts b/x-pack/typings/global_fetch.d.ts new file mode 100644 index 00000000000000..b2be5f7f245675 --- /dev/null +++ b/x-pack/typings/global_fetch.d.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. + */ + +// This type needs to still exist due to apollo-link-http-common hasn't yet updated +// it's usage (https://github.com/apollographql/apollo-link/issues/1131) +declare type GlobalFetch = WindowOrWorkerGlobalScope; diff --git a/yarn.lock b/yarn.lock index 6da8e810b6f56f..5bb6bb3da2e47c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3930,7 +3930,7 @@ resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.13.tgz#ca039c23a9e27ebea53e0901ef928ea2a1a6d313" integrity sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung== -"@types/sizzle@*": +"@types/sizzle@*", "@types/sizzle@2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== @@ -4191,24 +4191,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.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.5.0.tgz#101d96743ce3365b3223df73d641078c9b775903" - integrity sha512-ddrJZxp5ns1Lh5ofZQYk3P8RyvKfyz/VcRR4ZiJLHO/ljnQAO8YvTfj268+WJOOadn99mvDiqJA65+HAKoeSPA== +"@typescript-eslint/eslint-plugin@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.7.0.tgz#dff176bdb73dfd7e2e43062452189bd1b9db6021" + integrity sha512-H5G7yi0b0FgmqaEUpzyBlVh0d9lq4cWG2ap0RKa6BkF3rpBb6IrAoubt1NWh9R2kRs/f0k6XwRDiDz3X/FqXhQ== dependencies: - "@typescript-eslint/experimental-utils" "2.5.0" + "@typescript-eslint/experimental-utils" "2.7.0" eslint-utils "^1.4.2" functional-red-black-tree "^1.0.1" regexpp "^2.0.1" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.5.0.tgz#383a97ded9a7940e5053449f6d73995e782b8fb1" - integrity sha512-UgcQGE0GKJVChyRuN1CWqDW8Pnu7+mVst0aWrhiyuUD1J9c+h8woBdT4XddCvhcXDodTDVIfE3DzGHVjp7tUeQ== +"@typescript-eslint/experimental-utils@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.7.0.tgz#58d790a3884df3041b5a5e08f9e5e6b7c41864b5" + integrity sha512-9/L/OJh2a5G2ltgBWJpHRfGnt61AgDeH6rsdg59BH0naQseSwR7abwHq3D5/op0KYD/zFT4LS5gGvWcMmegTEg== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.5.0" + "@typescript-eslint/typescript-estree" "2.7.0" eslint-scope "^5.0.0" "@typescript-eslint/experimental-utils@^1.13.0": @@ -4220,14 +4220,14 @@ "@typescript-eslint/typescript-estree" "1.13.0" eslint-scope "^4.0.0" -"@typescript-eslint/parser@^2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.5.0.tgz#858030ddd808fbbe88e03f42e5971efaccb8218a" - integrity sha512-9UBMiAwIDWSl79UyogaBdj3hidzv6exjKUx60OuZuFnJf56tq/UMpdPcX09YmGqE8f4AnAueYtBxV8IcAT3jdQ== +"@typescript-eslint/parser@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.7.0.tgz#b5e6a4944e2b68dba1e7fbfd5242e09ff552fd12" + integrity sha512-ctC0g0ZvYclxMh/xI+tyqP0EC2fAo6KicN9Wm2EIao+8OppLfxji7KAGJosQHSGBj3TcqUrA96AjgXuKa5ob2g== dependencies: "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.5.0" - "@typescript-eslint/typescript-estree" "2.5.0" + "@typescript-eslint/experimental-utils" "2.7.0" + "@typescript-eslint/typescript-estree" "2.7.0" eslint-visitor-keys "^1.1.0" "@typescript-eslint/typescript-estree@1.13.0": @@ -4238,16 +4238,17 @@ lodash.unescape "4.0.1" semver "5.5.0" -"@typescript-eslint/typescript-estree@2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.5.0.tgz#40ada624d6217ef092a3a79ed30d947ad4f212ce" - integrity sha512-AXURyF8NcA3IsnbjNX1v9qbwa0dDoY9YPcKYR2utvMHoUcu3636zrz0gRWtVAyxbPCkhyKuGg6WZIyi2Fc79CA== +"@typescript-eslint/typescript-estree@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.7.0.tgz#34fd98c77a07b40d04d5b4203eddd3abeab909f4" + integrity sha512-vVCE/DY72N4RiJ/2f10PTyYekX2OLaltuSIBqeHYI44GQ940VCYioInIb8jKMrK9u855OEJdFC+HmWAZTnC+Ag== dependencies: debug "^4.1.1" glob "^7.1.4" is-glob "^4.0.1" lodash.unescape "4.0.1" semver "^6.3.0" + tsutils "^3.17.1" "@typescript-eslint/typescript-estree@^1.9.0": version "1.9.0" @@ -5125,20 +5126,23 @@ apollo-link-http-common@^0.2.13: ts-invariant "^0.3.2" tslib "^1.9.3" -apollo-link-http-common@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.4.tgz#877603f7904dc8f70242cac61808b1f8d034b2c3" - integrity sha512-4j6o6WoXuSPen9xh4NBaX8/vL98X1xY2cYzUEK1F8SzvHe2oFONfxJBTekwU8hnvapcuq8Qh9Uct+gelu8T10g== +apollo-link-http-common@^0.2.15: + version "0.2.15" + resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.15.tgz#304e67705122bf69a9abaded4351b10bc5efd6d9" + integrity sha512-+Heey4S2IPsPyTf8Ag3PugUupASJMW894iVps6hXbvwtg1aHSNMXUYO5VG7iRHkPzqpuzT4HMBanCTXPjtGzxg== dependencies: - apollo-link "^1.2.2" + apollo-link "^1.2.13" + ts-invariant "^0.4.0" + tslib "^1.9.3" -apollo-link-http@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.4.tgz#b80b7b4b342c655b6a5614624b076a36be368f43" - integrity sha512-e9Ng3HfnW00Mh3TI6DhNRfozmzQOtKgdi+qUAsHBOEcTP0PTAmb+9XpeyEEOueLyO0GXhB92HUCIhzrWMXgwyg== +apollo-link-http@^1.5.16: + version "1.5.16" + resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.16.tgz#44fe760bcc2803b8a7f57fc9269173afb00f3814" + integrity sha512-IA3xA/OcrOzINRZEECI6IdhRp/Twom5X5L9jMehfzEo2AXdeRwAMlH5LuvTZHgKD8V1MBnXdM6YXawXkTDSmJw== dependencies: - apollo-link "^1.2.2" - apollo-link-http-common "^0.2.4" + apollo-link "^1.2.13" + apollo-link-http-common "^0.2.15" + tslib "^1.9.3" apollo-link-schema@^1.1.0: version "1.1.0" @@ -5173,6 +5177,16 @@ apollo-link@^1.2.11: tslib "^1.9.3" zen-observable-ts "^0.8.18" +apollo-link@^1.2.13: + version "1.2.13" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.13.tgz#dff00fbf19dfcd90fddbc14b6a3f9a771acac6c4" + integrity sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw== + dependencies: + apollo-utilities "^1.3.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + zen-observable-ts "^0.8.20" + apollo-server-core@^1.3.6: version "1.3.6" resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-1.3.6.tgz#08636243c2de56fa8c267d68dd602cb1fbd323e3" @@ -5224,7 +5238,7 @@ apollo-utilities@^1.2.1: ts-invariant "^0.2.1" tslib "^1.9.3" -apollo-utilities@^1.3.2: +apollo-utilities@^1.3.0, apollo-utilities@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9" integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg== @@ -9178,13 +9192,14 @@ cyclist@~0.2.2: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= -cypress@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.4.1.tgz#ca2e4e9864679da686c6a6189603efd409664c30" - integrity sha512-1HBS7t9XXzkt6QHbwfirWYty8vzxNMawGj1yI+Fu6C3/VZJ8UtUngMW6layqwYZzLTZV8tiDpdCNBypn78V4Dg== +cypress@^3.5.5: + version "3.6.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-3.6.1.tgz#4420957923879f60b7a5146ccbf81841a149b653" + integrity sha512-6n0oqENdz/oQ7EJ6IgESNb2M7Bo/70qX9jSJsAziJTC3kICfEMmJUlrAnP9bn+ut24MlXQST5nRXhUP5nRIx6A== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" "@cypress/xvfb" "1.2.4" + "@types/sizzle" "2.3.2" arch "2.1.1" bluebird "3.5.0" cachedir "1.3.0" @@ -9211,6 +9226,7 @@ cypress@^3.4.1: request-progress "3.0.0" supports-color "5.5.0" tmp "0.1.0" + untildify "3.0.3" url "0.11.0" yauzl "2.10.0" @@ -27891,10 +27907,10 @@ typescript-fsa@^2.0.0, typescript-fsa@^2.5.0: resolved "https://registry.yarnpkg.com/typescript-fsa/-/typescript-fsa-2.5.0.tgz#1baec01b5e8f5f34c322679d1327016e9e294faf" integrity sha1-G67AG16PXzTDImedEycBbp4pT68= -typescript@3.5.3, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.3.3333, typescript@~3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" - integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== +typescript@3.5.3, typescript@3.7.2, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.3.3333, typescript@~3.5.3: + version "3.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" + integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== typings-tester@^0.3.2: version "0.3.2" @@ -28213,6 +28229,11 @@ unstated@^2.1.1: dependencies: create-react-context "^0.1.5" +untildify@3.0.3, untildify@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" + integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA== + untildify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0" @@ -28220,11 +28241,6 @@ untildify@^2.0.0: dependencies: os-homedir "^1.0.0" -untildify@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" - integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA== - unzip-response@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" @@ -30505,6 +30521,14 @@ zen-observable-ts@^0.8.18: tslib "^1.9.3" zen-observable "^0.8.0" +zen-observable-ts@^0.8.20: + version "0.8.20" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz#44091e335d3fcbc97f6497e63e7f57d5b516b163" + integrity sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA== + dependencies: + tslib "^1.9.3" + zen-observable "^0.8.0" + zen-observable@^0.8.0: version "0.8.8" resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.8.tgz#1ea93995bf098754a58215a1e0a7309e5749ec42" From 1a8bb3a2231f1809d32b8ca2070b145c13c84fec Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Fri, 15 Nov 2019 10:05:58 +0000 Subject: [PATCH 17/28] [ML] Fixes word wrap in Overview page sidebar on IE (#50668) --- .../ml/public/overview/components/sidebar.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/overview/components/sidebar.tsx b/x-pack/legacy/plugins/ml/public/overview/components/sidebar.tsx index 496beb158f6984..82caadd4d58ea9 100644 --- a/x-pack/legacy/plugins/ml/public/overview/components/sidebar.tsx +++ b/x-pack/legacy/plugins/ml/public/overview/components/sidebar.tsx @@ -25,13 +25,13 @@ function getCreateJobLink(createAnomalyDetectionJobDisabled: boolean) { return createAnomalyDetectionJobDisabled === true ? ( ) : ( ); @@ -43,15 +43,13 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }

@@ -87,13 +85,13 @@ export const OverviewSideBar: FC = ({ createAnomalyDetectionJobDisabled }

), From 0c42aa8a58f88ae14d7e3efda65f694a92e0c729 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 15 Nov 2019 12:56:24 +0100 Subject: [PATCH 18/28] [ML] Add the job message tab to data frame analytics (#50468) * [ML] get analytics job messages from ml-notifications * [ML] remove commented import * [ML] add job_type filter for anomaly_detector * [ML] job messages component * [ML] update styles * [ML] update job messages container for anomaly detection with async await * [ML] fix passing a prop * [ML] fix types and error message * [ML] fix i18n * [ML] fix text alignment * [ML] remove duplicated copyright comment --- .../ml/common/constants/index_patterns.ts | 1 - .../plugins/ml/common/types/audit_message.ts | 4 - .../job_message_icon/{index.js => index.ts} | 1 - .../job_message_icon/job_message_icon.tsx | 2 +- .../public/components/job_messages/index.ts | 7 ++ .../components/job_messages/job_messages.tsx | 73 +++++++++++++++ .../analytics_list/expanded_row.tsx | 7 +- .../expanded_row_messages_pane.tsx | 56 ++---------- .../components/job_details/_job_details.scss | 45 +++------- .../components/job_details/job_details.js | 2 +- .../job_details/job_messages_pane.js | 89 ------------------- .../job_details/job_messages_pane.tsx | 40 +++++++++ .../jobs_list_page/_expanded_row.scss | 44 +++------ .../public/services/ml_api_service/index.d.ts | 3 +- .../analytics_audit_messages.ts | 19 ++-- .../job_audit_messages/job_audit_messages.js | 5 ++ .../translations/translations/ja-JP.json | 6 -- .../translations/translations/zh-CN.json | 6 -- 18 files changed, 176 insertions(+), 234 deletions(-) rename x-pack/legacy/plugins/ml/public/components/job_message_icon/{index.js => index.ts} (99%) create mode 100644 x-pack/legacy/plugins/ml/public/components/job_messages/index.ts create mode 100644 x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.js create mode 100644 x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx diff --git a/x-pack/legacy/plugins/ml/common/constants/index_patterns.ts b/x-pack/legacy/plugins/ml/common/constants/index_patterns.ts index 9b87f69ad1b6b4..4a0dfdde04ad49 100644 --- a/x-pack/legacy/plugins/ml/common/constants/index_patterns.ts +++ b/x-pack/legacy/plugins/ml/common/constants/index_patterns.ts @@ -10,4 +10,3 @@ export const ML_ANNOTATIONS_INDEX_PATTERN = '.ml-annotations-6'; export const ML_RESULTS_INDEX_PATTERN = '.ml-anomalies-*'; export const ML_NOTIFICATION_INDEX_PATTERN = '.ml-notifications*'; -export const ML_DF_NOTIFICATION_INDEX_PATTERN = '.data-frame-notifications-1'; diff --git a/x-pack/legacy/plugins/ml/common/types/audit_message.ts b/x-pack/legacy/plugins/ml/common/types/audit_message.ts index 58e77495b90937..a0ac92417dfe6f 100644 --- a/x-pack/legacy/plugins/ml/common/types/audit_message.ts +++ b/x-pack/legacy/plugins/ml/common/types/audit_message.ts @@ -12,10 +12,6 @@ export interface AuditMessageBase { text?: string; } -export interface AnalyticsMessage extends AuditMessageBase { - analytics_id: string; -} - export interface JobMessage extends AuditMessageBase { job_id: string; } diff --git a/x-pack/legacy/plugins/ml/public/components/job_message_icon/index.js b/x-pack/legacy/plugins/ml/public/components/job_message_icon/index.ts similarity index 99% rename from x-pack/legacy/plugins/ml/public/components/job_message_icon/index.js rename to x-pack/legacy/plugins/ml/public/components/job_message_icon/index.ts index a92fd6c8ae0584..c307df90ae8cec 100644 --- a/x-pack/legacy/plugins/ml/public/components/job_message_icon/index.js +++ b/x-pack/legacy/plugins/ml/public/components/job_message_icon/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - export { JobIcon } from './job_message_icon'; diff --git a/x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx b/x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx index 59e71974d0aed7..545e9231699fd2 100644 --- a/x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx +++ b/x-pack/legacy/plugins/ml/public/components/job_message_icon/job_message_icon.tsx @@ -11,7 +11,7 @@ import { AuditMessageBase } from '../../../common/types/audit_message'; interface Props { message: AuditMessageBase; - showTooltip: boolean; + showTooltip?: boolean; } const [INFO, WARNING, ERROR] = ['info', 'warning', 'error']; diff --git a/x-pack/legacy/plugins/ml/public/components/job_messages/index.ts b/x-pack/legacy/plugins/ml/public/components/job_messages/index.ts new file mode 100644 index 00000000000000..dee6aa66694a62 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/job_messages/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { JobMessages } from './job_messages'; diff --git a/x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx b/x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx new file mode 100644 index 00000000000000..08f9a4379559b6 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/job_messages/job_messages.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiSpacer, EuiBasicTable } from '@elastic/eui'; +// @ts-ignore +import { formatDate } from '@elastic/eui/lib/services/format'; +import { i18n } from '@kbn/i18n'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; + +import { JobMessage } from '../../../common/types/audit_message'; +import { JobIcon } from '../job_message_icon'; + +const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; + +interface JobMessagesProps { + messages: JobMessage[]; + loading: boolean; + error: string; +} + +/** + * Component for rendering job messages for anomaly detection + * and data frame analytics jobs. + */ +export const JobMessages: FC = ({ messages, loading, error }) => { + const columns = [ + { + name: '', + render: (message: JobMessage) => , + width: `${theme.euiSizeL}`, + }, + { + name: i18n.translate('xpack.ml.jobMessages.timeLabel', { + defaultMessage: 'Time', + }), + render: (message: any) => formatDate(message.timestamp, TIME_FORMAT), + width: '120px', + }, + { + field: 'node_name', + name: i18n.translate('xpack.ml.jobMessages.nodeLabel', { + defaultMessage: 'Node', + }), + width: '150px', + }, + { + field: 'message', + name: i18n.translate('xpack.ml.jobMessages.messageLabel', { + defaultMessage: 'Message', + }), + width: '50%', + }, + ]; + + return ( + <> + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index bfa21e503d5aab..2ad81a05741c1e 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -27,7 +27,7 @@ import { } from '../../../../common'; import { isCompletedAnalyticsJob } from './common'; import { isRegressionAnalysis } from '../../../../common/analytics'; -// import { ExpandedRowMessagesPane } from './expanded_row_messages_pane'; +import { ExpandedRowMessagesPane } from './expanded_row_messages_pane'; function getItemDescription(value: any) { if (typeof value === 'object') { @@ -235,19 +235,16 @@ export const ExpandedRow: FC = ({ item }) => { name: 'JSON', content: , }, - // Audit messages are not yet supported by the analytics API. - /* { id: 'ml-analytics-job-messages', name: i18n.translate( 'xpack.ml.dataframe.analyticsList.analyticsDetails.tabs.analyticsMessagesLabel', { - defaultMessage: 'Messages', + defaultMessage: 'Job messages', } ), content: , }, - */ ]; // Using `expand=false` here so the tabs themselves don't spread diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx index 1751cf1a396460..e639f32116d4ae 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx @@ -4,27 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useState } from 'react'; - -import { EuiSpacer, EuiBasicTable } from '@elastic/eui'; -// @ts-ignore -import { formatDate } from '@elastic/eui/lib/services/format'; +import React, { FC, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; import { ml } from '../../../../../services/ml_api_service'; -// @ts-ignore -import { JobIcon } from '../../../../../components/job_message_icon'; -import { AnalyticsMessage } from '../../../../../../common/types/audit_message'; import { useRefreshAnalyticsList } from '../../../../common'; - -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; +import { JobMessages } from '../../../../../components/job_messages'; +import { JobMessage } from '../../../../../../common/types/audit_message'; interface Props { analyticsId: string; } export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { - const [messages, setMessages] = useState([]); + const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); @@ -63,43 +55,5 @@ export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { useRefreshAnalyticsList({ onRefresh: getMessagesFactory() }); - const columns = [ - { - name: '', - render: (message: AnalyticsMessage) => , - width: `${theme.euiSizeXL}px`, - }, - { - name: i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.timeLabel', { - defaultMessage: 'Time', - }), - render: (message: any) => formatDate(message.timestamp, TIME_FORMAT), - }, - { - field: 'node_name', - name: i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.nodeLabel', { - defaultMessage: 'Node', - }), - }, - { - field: 'message', - name: i18n.translate('xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.messageLabel', { - defaultMessage: 'Message', - }), - width: '50%', - }, - ]; - - return ( - - - - - ); + return ; }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss index 022c3513d09e18..1f68ec67ded471 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/_job_details.scss @@ -1,10 +1,9 @@ - .tab-contents { margin: -$euiSizeS; padding: $euiSizeS; background-color: $euiColorEmptyShade; - // SASSTODO: Need to remove bootstrap grid + // SASSTODO: Need to remove bootstrap grid .col-md-6:nth-child(1) { // SASSTODO: Why is this 7? padding-right: 7px; @@ -65,40 +64,24 @@ } } - // SASSTODO: This needs a proper calc + // SASSTODO: This needs a proper calc .json-textarea { height: 500px; } +} - // SASSTODO: This needs to be rewritten. A lot of this should be done with the JS props - .job-messages-table { - max-height: 500px; - overflow: auto; +.job-messages-table { + max-height: 500px; + overflow: auto; + text-align: left; - .euiTable { - font-size: 12px; - - th:nth-child(1) { - width: $euiSizeXL; - } - th:nth-child(2) { - width: 150px; - } - th:nth-child(3) { - width: 120px; - } - th:nth-child(4) { - width: auto; - } - tr:last-child { - td { - border-bottom: none; - } - } - - .euiTableRowCell { - background-color: $euiColorEmptyShade; - } + tr:last-child { + td { + border-bottom: none; } } + + .euiTableRowCell { + background-color: $euiColorEmptyShade; + } } diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js index 192310937c0e93..acec94efc37894 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js @@ -118,7 +118,7 @@ class JobDetailsUI extends Component { id: 'xpack.ml.jobsList.jobDetails.tabs.jobMessagesLabel', defaultMessage: 'Job messages' }), - content: , + content: , }, ]; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.js deleted file mode 100644 index 8015c38cfc8057..00000000000000 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - -import PropTypes from 'prop-types'; -import React, { - Component -} from 'react'; - -import { - EuiSpacer, - EuiBasicTable, -} from '@elastic/eui'; - -import { formatDate } from '@elastic/eui/lib/services/format'; -import { ml } from 'plugins/ml/services/ml_api_service'; -import { JobIcon } from '../../../../components/job_message_icon'; -import { injectI18n } from '@kbn/i18n/react'; - -const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; - -class JobMessagesPaneUI extends Component { - - constructor(props) { - super(props); - - this.state = { - messages: [] - }; - this.jobId = props.job.job_id; - } - - componentDidMount() { - ml.jobs.jobAuditMessages(this.jobId) - .then((messages) => { - this.setState({ messages }); - }) - .catch((error) => { - console.log('Job messages could not be loaded', error); - }); - } - - render() { - const { messages } = this.state; - const { intl } = this.props; - const columns = [{ - name: '', - render: item => () - }, { - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.messagesPane.timeLabel', - defaultMessage: 'Time' - }), - render: item => formatDate(item.timestamp, TIME_FORMAT) - }, { - field: 'node_name', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.messagesPane.nodeLabel', - defaultMessage: 'Node' - }), - }, { - field: 'message', - name: intl.formatMessage({ - id: 'xpack.ml.jobsList.jobDetails.messagesPane.messageLabel', - defaultMessage: 'Message' - }), - } - ]; - return ( - - -

- -
- - ); - } -} -JobMessagesPaneUI.propTypes = { - job: PropTypes.object.isRequired, -}; - -export const JobMessagesPane = injectI18n(JobMessagesPaneUI); diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx new file mode 100644 index 00000000000000..ca80012767c2d0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_messages_pane.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useEffect, useState } from 'react'; + +import { ml } from '../../../../services/ml_api_service'; +import { JobMessages } from '../../../../components/job_messages'; +import { JobMessage } from '../../../../../common/types/audit_message'; + +interface JobMessagesPaneProps { + jobId: string; +} + +export const JobMessagesPane: FC = ({ jobId }) => { + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + + const fetchMessages = async () => { + setIsLoading(true); + try { + setMessages(await ml.jobs.jobAuditMessages(jobId)); + setIsLoading(false); + } catch (e) { + setIsLoading(false); + setErrorMessage(e); + // eslint-disable-next-line no-console + console.error('Job messages could not be loaded', e); + } + }; + + useEffect(() => { + fetchMessages(); + }, []); + + return ; +}; diff --git a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_expanded_row.scss b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_expanded_row.scss index e21d7d6c0e1db0..aeddbbd1a51082 100644 --- a/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_expanded_row.scss +++ b/x-pack/legacy/plugins/ml/public/management/jobs_list/components/jobs_list_page/_expanded_row.scss @@ -4,7 +4,7 @@ padding: $euiSizeS; background-color: $euiColorEmptyShade; - // SASSTODO: Need to remove bootstrap grid + // SASSTODO: Need to remove bootstrap grid .col-md-6:nth-child(1) { // SASSTODO: Why is this 7? padding-right: 7px; @@ -65,40 +65,24 @@ } } - // SASSTODO: This needs a proper calc + // SASSTODO: This needs a proper calc .json-textarea { height: 500px; } +} - // SASSTODO: This needs to be rewritten. A lot of this should be done with the JS props - .job-messages-table { - max-height: 500px; - overflow: auto; - - .euiTable { - font-size: 12px; - - th:nth-child(1) { - width: $euiSizeXL; - } - th:nth-child(2) { - width: 150px; - } - th:nth-child(3) { - width: 120px; - } - th:nth-child(4) { - width: auto; - } - tr:last-child { - td { - border-bottom: none; - } - } +.job-messages-table { + max-height: 500px; + overflow: auto; + text-align: left; - .euiTableRowCell { - background-color: $euiColorEmptyShade; - } + tr:last-child { + td { + border-bottom: none; } } + + .euiTableRowCell { + background-color: $euiColorEmptyShade; + } } diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts index 38a71d994c601e..414229578c2177 100644 --- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts @@ -12,6 +12,7 @@ import { MlSummaryJobs } from '../../../common/types/jobs'; import { MlServerDefaults, MlServerLimits } from '../../services/ml_server_info'; import { ES_AGGREGATION } from '../../../common/constants/aggregation_types'; import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { JobMessage } from '../../../common/types/audit_message'; // TODO This is not a complete representation of all methods of `ml.*`. // It just satisfies needs for other parts of the code area which use @@ -117,7 +118,7 @@ declare interface Ml { stopDatafeeds(datafeedIds: string[]): Promise; deleteJobs(jobIds: string[]): Promise; closeJobs(jobIds: string[]): Promise; - jobAuditMessages(jobId: string, from: string): Promise; + jobAuditMessages(jobId: string, from?: string): Promise; deletingJobTasks(): Promise; newJobCaps(indexPatternTitle: string, isRollup: boolean): Promise; newJobLineChart( diff --git a/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts b/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts index c0a4e639f64347..abe389165182f0 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts +++ b/x-pack/legacy/plugins/ml/server/models/data_frame_analytics/analytics_audit_messages.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ML_DF_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; import { callWithRequestType } from '../../../common/types/kibana'; -import { AnalyticsMessage } from '../../../common/types/audit_message'; +import { ML_NOTIFICATION_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { JobMessage } from '../../../common/types/audit_message'; const SIZE = 50; @@ -15,7 +15,7 @@ interface Message { _type: string; _id: string; _score: null | number; - _source: AnalyticsMessage; + _source: JobMessage; sort?: any; } @@ -37,6 +37,11 @@ export function analyticsAuditMessagesProvider(callWithRequest: callWithRequestT level: 'activity', }, }, + must: { + term: { + job_type: 'data_frame_analytics', + }, + }, }, }, ], @@ -50,12 +55,12 @@ export function analyticsAuditMessagesProvider(callWithRequest: callWithRequestT should: [ { term: { - analytics_id: '', // catch system messages + job_id: '', // catch system messages }, }, { term: { - analytics_id: analyticsId, // messages for specified analyticsId + job_id: analyticsId, // messages for specified analyticsId }, }, ], @@ -65,12 +70,12 @@ export function analyticsAuditMessagesProvider(callWithRequest: callWithRequestT try { const resp = await callWithRequest('search', { - index: ML_DF_NOTIFICATION_INDEX_PATTERN, + index: ML_NOTIFICATION_INDEX_PATTERN, ignore_unavailable: true, rest_total_hits_as_int: true, size: SIZE, body: { - sort: [{ timestamp: { order: 'desc' } }, { analytics_id: { order: 'asc' } }], + sort: [{ timestamp: { order: 'desc' } }, { job_id: { order: 'asc' } }], query, }, }); diff --git a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js index 891990a84756ac..3b666cad7d8eb3 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/legacy/plugins/ml/server/models/job_audit_messages/job_audit_messages.js @@ -48,6 +48,11 @@ export function jobAuditMessagesProvider(callWithRequest) { term: { level: 'activity' } + }, + must: { + term: { + job_type: 'anomaly_detector' + } } } }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 34491036e5a453..872e09ea97d3b6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6130,9 +6130,6 @@ "xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel": "表示", "xpack.ml.jobsList.jobDetails.generalTitle": "一般", "xpack.ml.jobsList.jobDetails.influencersTitle": "影響", - "xpack.ml.jobsList.jobDetails.messagesPane.messageLabel": "メッセージ", - "xpack.ml.jobsList.jobDetails.messagesPane.nodeLabel": "ノード", - "xpack.ml.jobsList.jobDetails.messagesPane.timeLabel": "時間", "xpack.ml.jobsList.jobDetails.modelSizeStatsTitle": "モデルサイズ統計", "xpack.ml.jobsList.jobDetails.nodeTitle": "ノード", "xpack.ml.jobsList.jobDetails.noPermissionToViewDatafeedPreviewTitle": "データフィードのプレビューを表示するパーミッションがありません", @@ -6610,9 +6607,6 @@ "xpack.ml.datavisualizer.searchPanel.sampleLabel": "サンプル", "xpack.ml.datavisualizer.searchPanel.sampleSizeAriaLabel": "サンプリングするドキュメント数を選択してください", "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage": "メッセージを読み込めませんでした", - "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.messageLabel": "メッセージ", - "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.nodeLabel": "ノード", - "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.timeLabel": "時間", "xpack.ml.fieldDataCard.cardBoolean.documentsCountDescription": "{count, plural, zero {# document} one {# document} other {# documents}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardDate.documentsCountDescription": "{count, plural, zero {# document} one {# document} other {# documents}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardDate.earliestDescription": "最も古い {earliestFormatted}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index df00be9685ccfe..25ad15d05ea167 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6131,9 +6131,6 @@ "xpack.ml.jobsList.jobDetails.forecastsTable.viewLabel": "查看", "xpack.ml.jobsList.jobDetails.generalTitle": "常规", "xpack.ml.jobsList.jobDetails.influencersTitle": "影响因素", - "xpack.ml.jobsList.jobDetails.messagesPane.messageLabel": "消息", - "xpack.ml.jobsList.jobDetails.messagesPane.nodeLabel": "节点", - "xpack.ml.jobsList.jobDetails.messagesPane.timeLabel": "时间", "xpack.ml.jobsList.jobDetails.modelSizeStatsTitle": "模型大小统计", "xpack.ml.jobsList.jobDetails.nodeTitle": "节点", "xpack.ml.jobsList.jobDetails.noPermissionToViewDatafeedPreviewTitle": "您无权查看数据馈送预览", @@ -6703,9 +6700,6 @@ "xpack.ml.datavisualizer.searchPanel.sampleLabel": "采样", "xpack.ml.datavisualizer.searchPanel.sampleSizeAriaLabel": "选择要采样的文档数目", "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.errorMessage": "无法加载消息", - "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.messageLabel": "消息", - "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.nodeLabel": "节点", - "xpack.ml.dfAnalyticsList.analyticsDetails.messagesPane.timeLabel": "时间", "xpack.ml.fieldDataCard.cardBoolean.documentsCountDescription": "{count, plural, zero {# 个文档} one {# 个文档} other {# 个文档}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardDate.documentsCountDescription": "{count, plural, zero {# 个文档} one {# 个文档} other {# 个文档}} ({docsPercent}%)", "xpack.ml.fieldDataCard.cardDate.earliestDescription": "最早的 {earliestFormatted}", From c982b8f4dcf6d5292cd48b932b9fdb29a81fccf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 15 Nov 2019 13:26:57 +0100 Subject: [PATCH 19/28] [APM] Update Error occurrences graph tooltip to display start and end for bucket period (#49638) * Adding end time inside error tooltip * changing end time precision * refactoring * refactoring * pr comments refactoring * pr comments refactoring * pr comments refactoring * pr comments refactoring * renaming some functions to make it more clear * Refactoring date difference range * refactoring transformers file * refactoring date time formatters * refactoring formatters into a new folder * refactoring getDurationUnit * refactoring duration formatter * fixing unit test * refactoring unit test * Adding timezone to tests * fixing translation issue * fixing translation issue * improving code * exporting toMicroseconds * removing unused import * refactoring duration * refactoring duration * fixing unit test * fixing unit test --- .../ErrorGroupDetails/Distribution/index.tsx | 5 + .../app/ErrorGroupOverview/List/index.tsx | 2 +- .../app/ServiceOverview/ServiceList/index.tsx | 8 +- .../AgentConfigurationList.tsx | 2 +- .../app/TraceOverview/TraceList.tsx | 8 +- .../TransactionDetails/Distribution/index.tsx | 17 +- .../Waterfall/WaterfallItem.tsx | 4 +- .../app/TransactionOverview/List/index.tsx | 12 +- .../shared/Summary/DurationSummaryItem.tsx | 4 +- .../components/shared/Summary/index.tsx | 3 +- .../TimestampTooltip/__test__/index.test.tsx | 61 +++++ .../shared/TimestampTooltip/index.test.tsx | 108 -------- .../shared/TimestampTooltip/index.tsx | 36 +-- .../TransactionBreakdownGraph/index.tsx | 3 +- .../Histogram/__test__/Histogram.test.js | 18 +- .../shared/charts/MetricsChart/index.tsx | 23 +- .../shared/charts/Timeline/AgentMarker.js | 4 +- .../shared/charts/Timeline/TimelineAxis.js | 9 +- .../components/shared/charts/Tooltip/index.js | 6 +- .../ChoroplethMap/ChoroplethToolTip.tsx | 4 +- .../shared/charts/TransactionCharts/index.tsx | 12 +- .../apm/public/selectors/chartSelectors.ts | 8 +- .../plugins/apm/public/utils/flattenObject.ts | 3 +- .../plugins/apm/public/utils/formatters.ts | 247 ------------------ .../formatters/__test__/datetime.test.ts | 146 +++++++++++ .../formatters/__test__/duration.test.ts | 131 ++++++++++ .../formatters/__test__/formatters.test.ts | 31 +++ .../__test__/size.test.ts} | 56 +--- .../apm/public/utils/formatters/datetime.ts | 149 +++++++++++ .../apm/public/utils/formatters/duration.ts | 153 +++++++++++ .../apm/public/utils/formatters/formatters.ts | 38 +++ .../apm/public/utils/formatters/index.ts | 10 + .../apm/public/utils/formatters/size.ts | 67 +++++ .../public/utils/isValidCoordinateValue.ts | 6 +- x-pack/legacy/plugins/apm/typings/common.d.ts | 2 + .../legacy/plugins/apm/typings/timeseries.ts | 3 +- 36 files changed, 889 insertions(+), 510 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx delete mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts rename x-pack/legacy/plugins/apm/public/utils/{__test__/formatters.test.ts => formatters/__test__/size.test.ts} (63%) create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/index.ts create mode 100644 x-pack/legacy/plugins/apm/public/utils/formatters/size.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index 2667d03ef8dde4..daba164a4a00ce 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -10,6 +10,7 @@ import React from 'react'; // @ts-ignore import Histogram from '../../../shared/charts/Histogram'; import { EmptyMessage } from '../../../shared/EmptyMessage'; +import { asRelativeDateTimeRange } from '../../../../utils/formatters'; interface IBucket { key: number; @@ -51,6 +52,9 @@ interface Props { title: React.ReactNode; } +const tooltipHeader = (bucket: FormattedBucket) => + asRelativeDateTimeRange(bucket.x0, bucket.x); + export function ErrorDistribution({ distribution, title }: Props) { const buckets = getFormattedBuckets( distribution.buckets, @@ -73,6 +77,7 @@ export function ErrorDistribution({ distribution, title }: Props) { {title} bucket.x} xType="time" buckets={buckets} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx index 2f06f1d52de6ba..a6c80581585783 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -143,7 +143,7 @@ const ErrorGroupList: React.FC = props => { align: 'right', render: (value?: number) => value ? ( - + ) : ( NOT_AVAILABLE_LABEL ) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index f2524ef1c16f4c..13e7a5bfd894e2 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { fontSizes, truncate } from '../../../../style/variables'; -import { asDecimal, asMillis } from '../../../../utils/formatters'; +import { asDecimal, convertTo } from '../../../../utils/formatters'; import { ManagedTable } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; @@ -80,7 +80,11 @@ export const SERVICE_COLUMNS = [ }), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted }, { field: 'transactionsPerMinute', diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx index 161d3711484785..c660455e1eed83 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx @@ -128,7 +128,7 @@ export function AgentConfigurationList({ ), sortable: true, render: (value: number) => ( - + ) }, { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index ca10b06c11cbf2..9116e02870a806 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -10,7 +10,7 @@ import React from 'react'; import styled from 'styled-components'; import { ITransactionGroup } from '../../../../server/lib/transaction_groups/transform'; import { fontSizes, truncate } from '../../../style/variables'; -import { asMillis } from '../../../utils/formatters'; +import { convertTo } from '../../../utils/formatters'; import { EmptyMessage } from '../../shared/EmptyMessage'; import { ImpactBar } from '../../shared/ImpactBar'; import { TransactionDetailLink } from '../../shared/Links/apm/TransactionDetailLink'; @@ -66,7 +66,11 @@ const traceListColumns: Array> = [ }), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted }, { field: 'transactionsPerMinute', diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index fc86f4bb78afbe..c9e5175a109219 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -11,7 +11,7 @@ import React, { FunctionComponent, useCallback } from 'react'; import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { getTimeFormatter, timeUnit } from '../../../../utils/formatters'; +import { getDurationFormatter } from '../../../../utils/formatters'; // @ts-ignore import Histogram from '../../../shared/charts/Histogram'; import { EmptyMessage } from '../../../shared/EmptyMessage'; @@ -132,8 +132,7 @@ export const TransactionDistribution: FunctionComponent = ( ); const xMax = d3.max(buckets, d => d.x) || 0; - const timeFormatter = getTimeFormatter(xMax); - const unit = timeUnit(xMax); + const timeFormatter = getDurationFormatter(xMax); const bucketIndex = buckets.findIndex( bucket => @@ -187,18 +186,18 @@ export const TransactionDistribution: FunctionComponent = ( }); } }} - formatX={timeFormatter} + formatX={(time: number) => timeFormatter(time).formatted} formatYShort={formatYShort} formatYLong={formatYLong} verticalLineHover={(bucket: IChartPoint) => bucket.y > 0 && !bucket.sample } backgroundHover={(bucket: IChartPoint) => bucket.y > 0 && bucket.sample} - tooltipHeader={(bucket: IChartPoint) => - `${timeFormatter(bucket.x0, { - withUnit: false - })} - ${timeFormatter(bucket.x, { withUnit: false })} ${unit}` - } + tooltipHeader={(bucket: IChartPoint) => { + const xFormatted = timeFormatter(bucket.x); + const x0Formatted = timeFormatter(bucket.x0); + return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; + }} tooltipFooter={(bucket: IChartPoint) => !bucket.sample && i18n.translate( diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index a5e6eb622e8fb1..c64231a6ded863 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -12,7 +12,7 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { isRumAgentName } from '../../../../../../../common/agent_name'; import { px, unit, units } from '../../../../../../style/variables'; -import { asTime } from '../../../../../../utils/formatters'; +import { asDuration } from '../../../../../../utils/formatters'; import { ErrorCountBadge } from '../../ErrorCountBadge'; import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; @@ -133,7 +133,7 @@ const SpanActionToolTip: React.SFC = ({ function Duration({ item }: { item: IWaterfallItem }) { return ( - {asTime(item.duration)} + {asDuration(item.duration)} ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx index 062d103bfc4487..3d75011f52f193 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { ITransactionGroup } from '../../../../../server/lib/transaction_groups/transform'; import { fontFamilyCode, truncate } from '../../../../style/variables'; -import { asDecimal, asMillis } from '../../../../utils/formatters'; +import { asDecimal, convertTo } from '../../../../utils/formatters'; import { ImpactBar } from '../../../shared/ImpactBar'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; @@ -28,6 +28,12 @@ interface Props { isLoading: boolean; } +const toMilliseconds = (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted; + export function TransactionList({ items, isLoading }: Props) { const columns: Array> = useMemo( () => [ @@ -67,7 +73,7 @@ export function TransactionList({ items, isLoading }: Props) { ), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => toMilliseconds(time) }, { field: 'p95', @@ -79,7 +85,7 @@ export function TransactionList({ items, isLoading }: Props) { ), sortable: true, dataType: 'number', - render: (value: number) => asMillis(value) + render: (time: number) => toMilliseconds(time) }, { field: 'transactionsPerMinute', diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx index c76e62d987aaca..a5a677296825c4 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip, EuiText } from '@elastic/eui'; import { PercentOfParent } from '../../app/TransactionDetails/WaterfallWithSummmary/PercentOfParent'; -import { asTime } from '../../../utils/formatters'; +import { asDuration } from '../../../utils/formatters'; interface Props { duration: number; @@ -29,7 +29,7 @@ const DurationSummaryItem = ({ return ( <> - {asTime(duration)} + {asDuration(duration)}   diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx index c4b750a360efd3..ce6935d1858aac 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx @@ -8,9 +8,10 @@ import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { px, units } from '../../../../public/style/variables'; +import { Maybe } from '../../../../typings/common'; interface Props { - items: Array; + items: Array>; } // TODO: Light/Dark theme (@see https://github.com/elastic/kibana/issues/44840) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx new file mode 100644 index 00000000000000..b4678b287dc166 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import moment from 'moment-timezone'; +import { TimestampTooltip } from '../index'; +import { mockNow } from '../../../../utils/testHelpers'; + +describe('TimestampTooltip', () => { + const timestamp = 1570720000123; // Oct 10, 2019, 08:06:40.123 (UTC-7) + + beforeAll(() => { + // mock Date.now + mockNow(1570737000000); + + moment.tz.setDefault('America/Los_Angeles'); + }); + + afterAll(() => moment.tz.setDefault('')); + + it('should render component with relative time in body and absolute time in tooltip', () => { + expect(shallow()) + .toMatchInlineSnapshot(` + + 5 hours ago + + `); + }); + + it('should format with precision in milliseconds by default', () => { + expect( + shallow() + .find('EuiToolTip') + .prop('content') + ).toBe('Oct 10, 2019, 08:06:40.123 (UTC-7)'); + }); + + it('should format with precision in seconds', () => { + expect( + shallow() + .find('EuiToolTip') + .prop('content') + ).toBe('Oct 10, 2019, 08:06:40 (UTC-7)'); + }); + + it('should format with precision in minutes', () => { + expect( + shallow() + .find('EuiToolTip') + .prop('content') + ).toBe('Oct 10, 2019, 08:06 (UTC-7)'); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx deleted file mode 100644 index a7149c7604695f..00000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shallow } from 'enzyme'; -import React from 'react'; -import moment from 'moment-timezone'; -import { TimestampTooltip, asAbsoluteTime } from './index'; -import { mockNow } from '../../../utils/testHelpers'; - -describe('asAbsoluteTime', () => { - afterAll(() => moment.tz.setDefault('')); - - it('should add a leading plus for timezones with positive UTC offset', () => { - moment.tz.setDefault('Europe/Copenhagen'); - expect(asAbsoluteTime({ time: 1559390400000, precision: 'minutes' })).toBe( - 'Jun 1, 2019, 14:00 (UTC+2)' - ); - }); - - it('should add a leading minus for timezones with negative UTC offset', () => { - moment.tz.setDefault('America/Los_Angeles'); - expect(asAbsoluteTime({ time: 1559390400000, precision: 'minutes' })).toBe( - 'Jun 1, 2019, 05:00 (UTC-7)' - ); - }); - - it('should use default UTC offset formatting when offset contains minutes', () => { - moment.tz.setDefault('Canada/Newfoundland'); - expect(asAbsoluteTime({ time: 1559390400000, precision: 'minutes' })).toBe( - 'Jun 1, 2019, 09:30 (UTC-02:30)' - ); - }); - - it('should respect DST', () => { - moment.tz.setDefault('Europe/Copenhagen'); - const timeWithDST = 1559390400000; // Jun 1, 2019 - const timeWithoutDST = 1575201600000; // Dec 1, 2019 - - expect(asAbsoluteTime({ time: timeWithDST })).toBe( - 'Jun 1, 2019, 14:00:00.000 (UTC+2)' - ); - - expect(asAbsoluteTime({ time: timeWithoutDST })).toBe( - 'Dec 1, 2019, 13:00:00.000 (UTC+1)' - ); - }); -}); - -describe('TimestampTooltip', () => { - const timestamp = 1570720000123; // Oct 10, 2019, 08:06:40.123 (UTC-7) - - beforeAll(() => { - // mock Date.now - mockNow(1570737000000); - - moment.tz.setDefault('America/Los_Angeles'); - }); - - afterAll(() => moment.tz.setDefault('')); - - it('should render component with relative time in body and absolute time in tooltip', () => { - expect(shallow()) - .toMatchInlineSnapshot(` - - 5 hours ago - - `); - }); - - it('should format with precision in milliseconds by default', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019, 08:06:40.123 (UTC-7)'); - }); - - it('should format with precision in seconds', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019, 08:06:40 (UTC-7)'); - }); - - it('should format with precision in minutes', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019, 08:06 (UTC-7)'); - }); - - it('should format with precision in days', () => { - expect( - shallow() - .find('EuiToolTip') - .prop('content') - ).toBe('Oct 10, 2019 (UTC-7)'); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx index d7ef6517c2fb83..504ff36c078f0c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx @@ -6,48 +6,20 @@ import React from 'react'; import { EuiToolTip } from '@elastic/eui'; import moment from 'moment-timezone'; +import { asAbsoluteDateTime, TimeUnit } from '../../../utils/formatters'; interface Props { /** * timestamp in milliseconds */ time: number; - precision?: 'days' | 'minutes' | 'seconds' | 'milliseconds'; + timeUnit?: TimeUnit; } -function getPreciseTime(precision: Props['precision']) { - switch (precision) { - case 'days': - return ''; - case 'minutes': - return ', HH:mm'; - case 'seconds': - return ', HH:mm:ss'; - default: - return ', HH:mm:ss.SSS'; - } -} - -function withLeadingPlus(value: number) { - return value > 0 ? `+${value}` : value; -} - -export function asAbsoluteTime({ time, precision = 'milliseconds' }: Props) { - const momentTime = moment(time); - const utcOffsetHours = momentTime.utcOffset() / 60; - const utcOffsetFormatted = Number.isInteger(utcOffsetHours) - ? withLeadingPlus(utcOffsetHours) - : 'Z'; - - return momentTime.format( - `MMM D, YYYY${getPreciseTime(precision)} (UTC${utcOffsetFormatted})` - ); -} - -export function TimestampTooltip({ time, precision = 'milliseconds' }: Props) { +export function TimestampTooltip({ time, timeUnit = 'milliseconds' }: Props) { const momentTime = moment(time); const relativeTimeLabel = momentTime.fromNow(); - const absoluteTimeLabel = asAbsoluteTime({ time, precision }); + const absoluteTimeLabel = asAbsoluteDateTime(time, timeUnit); return ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx index 1bcf4e08c91443..c4e7ed86df8b7c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -9,6 +9,7 @@ import numeral from '@elastic/numeral'; import { throttle } from 'lodash'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; +import { Maybe } from '../../../../../typings/common'; import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart'; import { asPercent } from '../../../../utils/formatters'; import { unit } from '../../../../style/variables'; @@ -19,7 +20,7 @@ interface Props { timeseries: TimeSeries[]; } -const tickFormatY = (y: number | null | undefined) => { +const tickFormatY = (y: Maybe) => { return numeral(y || 0).format('0 %'); }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js index b511bdc4392277..f76a27480137a2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js @@ -11,9 +11,8 @@ import d3 from 'd3'; import { HistogramInner } from '../index'; import response from './response.json'; import { - getTimeFormatter, asDecimal, - timeUnit + getDurationFormatter } from '../../../../../utils/formatters'; import { toJson } from '../../../../../utils/testHelpers'; import { getFormattedBuckets } from '../../../../app/TransactionDetails/Distribution/index'; @@ -25,8 +24,7 @@ describe('Histogram', () => { beforeEach(() => { const buckets = getFormattedBuckets(response.buckets, response.bucketSize); const xMax = d3.max(buckets, d => d.x); - const timeFormatter = getTimeFormatter(xMax); - const unit = timeUnit(xMax); + const timeFormatter = getDurationFormatter(xMax); wrapper = mount( { bucketSize={response.bucketSize} transactionId="myTransactionId" onClick={onClick} - formatX={timeFormatter} + formatX={time => timeFormatter(time).formatted} formatYShort={t => `${asDecimal(t)} occ.`} formatYLong={t => `${asDecimal(t)} occurrences`} - tooltipHeader={bucket => - `${timeFormatter(bucket.x0, { - withUnit: false - })} - ${timeFormatter(bucket.x, { withUnit: false })} ${unit}` - } + tooltipHeader={bucket => { + const xFormatted = timeFormatter(bucket.x); + const x0Formatted = timeFormatter(bucket.x0); + return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; + }} width={800} /> ); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx index 51aa4a40fb9234..30dcc99af31b9a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx @@ -9,20 +9,21 @@ import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform // @ts-ignore import CustomPlot from '../CustomPlot'; import { - asDynamicBytes, + asDecimal, asPercent, + asInteger, + asDynamicBytes, getFixedByteFormatter, - asDecimal, - asTime, - asInteger + asDuration } from '../../../../utils/formatters'; import { Coordinate } from '../../../../../typings/timeseries'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { useChartsSync } from '../../../../hooks/useChartsSync'; +import { Maybe } from '../../../../../typings/common'; interface Props { - start: number | string | undefined; - end: number | string | undefined; + start: Maybe; + end: Maybe; chart: GenericMetricsChart; } @@ -64,17 +65,17 @@ function getYTickFormatter(chart: GenericMetricsChart) { return getFixedByteFormatter(max); } case 'percent': { - return (y: number | null | undefined) => asPercent(y || 0, 1); + return (y: Maybe) => asPercent(y || 0, 1); } case 'time': { - return (y: number | null | undefined) => asTime(y); + return (y: Maybe) => asDuration(y); } case 'integer': { - return (y: number | null | undefined) => + return (y: Maybe) => isValidCoordinateValue(y) ? asInteger(y) : y; } default: { - return (y: number | null | undefined) => + return (y: Maybe) => isValidCoordinateValue(y) ? asDecimal(y) : y; } } @@ -89,7 +90,7 @@ function getTooltipFormatter({ yUnit }: GenericMetricsChart) { return (c: Coordinate) => asPercent(c.y || 0, 1); } case 'time': { - return (c: Coordinate) => asTime(c.y); + return (c: Coordinate) => asDuration(c.y); } case 'integer': { return (c: Coordinate) => diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js index 1f8c6db8d20a8a..8ee23d61fe0eb6 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/AgentMarker.js @@ -10,7 +10,7 @@ import { EuiToolTip } from '@elastic/eui'; import Legend from '../Legend'; import { units, px } from '../../../../style/variables'; import styled from 'styled-components'; -import { asTime } from '../../../../utils/formatters'; +import { asDuration } from '../../../../utils/formatters'; import theme from '@elastic/eui/dist/eui_theme_light.json'; const NameContainer = styled.div` @@ -39,7 +39,7 @@ export default function AgentMarker({ agentMark, x }) { content={
{agentMark.name} - {asTime(agentMark.us)} + {asDuration(agentMark.us)}
} > diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js index 1648f427edd7d0..346aec9fb080a2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.js @@ -12,7 +12,7 @@ import { XYPlot, XAxis } from 'react-vis'; import LastTickValue from './LastTickValue'; import AgentMarker from './AgentMarker'; import { px } from '../../../../style/variables'; -import { getTimeFormatter } from '../../../../utils/formatters'; +import { getDurationFormatter } from '../../../../utils/formatters'; import theme from '@elastic/eui/dist/eui_theme_light.json'; // Remove any tick that is too close to topTraceDuration @@ -33,8 +33,9 @@ const getXAxisTickValues = (tickValues, topTraceDuration) => { function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) { const { margins, tickValues, width, xDomain, xMax, xScale } = plotValues; - const tickFormat = getTimeFormatter(xMax); + const tickFormatter = getDurationFormatter(xMax); const xAxisTickValues = getXAxisTickValues(tickValues, topTraceDuration); + const topTraceDurationFormatted = tickFormatter(topTraceDuration).formatted; return ( @@ -66,7 +67,7 @@ function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) { orientation="top" tickSize={0} tickValues={xAxisTickValues} - tickFormat={tickFormat} + tickFormat={time => tickFormatter(time).formatted} tickPadding={20} style={{ text: { fill: theme.euiColorDarkShade } @@ -76,7 +77,7 @@ function TimelineAxis({ plotValues, agentMarks, topTraceDuration }) { {topTraceDuration > 0 && ( )} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js index f5992ac7fc63b5..239e46c25904da 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js @@ -19,7 +19,7 @@ import { } from '../../../../style/variables'; import Legend from '../Legend'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { asAbsoluteTime } from '../../TimestampTooltip'; +import { asAbsoluteDateTime } from '../../../../utils/formatters'; const TooltipElm = styled.div` margin: 0 ${px(unit)}; @@ -87,9 +87,7 @@ export default function Tooltip({ return ( -
- {header || asAbsoluteTime({ time: x, precision: 'seconds' })} -
+
{header || asAbsoluteDateTime(x, 'seconds')}
{showLegends ? ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx index adcce161c7ac1c..d2b6970841bdc2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { asTime, asInteger } from '../../../../../utils/formatters'; +import { asDuration, asInteger } from '../../../../../utils/formatters'; import { fontSizes } from '../../../../../style/variables'; export const ChoroplethToolTip: React.SFC<{ @@ -26,7 +26,7 @@ export const ChoroplethToolTip: React.SFC<{ )}
- {asTime(value)} + {asDuration(value)}
( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 94f30a8a2325a6..b5894a9d91e4a0 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -27,13 +27,13 @@ import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { asInteger, tpmUnit, - TimeFormatter + TimeFormatter, + getDurationFormatter } from '../../../../utils/formatters'; import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; import { LicenseContext } from '../../../../context/LicenseContext'; import { TransactionLineChart } from './TransactionLineChart'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { getTimeFormatter } from '../../../../utils/formatters'; import { DurationByCountryMap } from './DurationByCountryMap'; import { TRANSACTION_PAGE_LOAD, @@ -74,12 +74,14 @@ export class TransactionCharts extends Component { }; public getResponseTimeTickFormatter = (formatter: TimeFormatter) => { - return (t: number) => formatter(t); + return (t: number) => formatter(t).formatted; }; public getResponseTimeTooltipFormatter = (formatter: TimeFormatter) => { return (p: Coordinate) => { - return isValidCoordinateValue(p.y) ? formatter(p.y) : NOT_AVAILABLE_LABEL; + return isValidCoordinateValue(p.y) + ? formatter(p.y).formatted + : NOT_AVAILABLE_LABEL; }; }; @@ -154,7 +156,7 @@ export class TransactionCharts extends Component { const { responseTimeSeries, tpmSeries } = charts; const { transactionType } = urlParams; const maxY = this.getMaxY(responseTimeSeries); - const formatter = getTimeFormatter(maxY); + const formatter = getDurationFormatter(maxY); return ( <> diff --git a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts index b15231e89365a3..75a558ac81a544 100644 --- a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts +++ b/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts @@ -16,7 +16,7 @@ import { RectCoordinate, TimeSeries } from '../../typings/timeseries'; -import { asDecimal, asMillis, tpmUnit } from '../utils/formatters'; +import { asDecimal, tpmUnit, convertTo } from '../utils/formatters'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { getEmptySeries } from '../components/shared/charts/CustomPlot/getEmptySeries'; import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor'; @@ -70,6 +70,10 @@ export function getResponseTimeSeries({ }: TimeSeriesAPIResponse) { const { overallAvgDuration } = apmTimeseries; const { avg, p95, p99 } = apmTimeseries.responseTimes; + const formattedDuration = convertTo({ + unit: 'milliseconds', + microseconds: overallAvgDuration + }).formatted; const series: TimeSeries[] = [ { @@ -77,7 +81,7 @@ export function getResponseTimeSeries({ defaultMessage: 'Avg.' }), data: avg, - legendValue: asMillis(overallAvgDuration), + legendValue: formattedDuration, type: 'linemark', color: theme.euiColorVis1 }, diff --git a/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts b/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts index 01a58ac03d0c3f..295ea1f9f900fd 100644 --- a/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts +++ b/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts @@ -5,6 +5,7 @@ */ import { compact, isObject } from 'lodash'; +import { Maybe } from '../../typings/common'; export interface KeyValuePair { key: string; @@ -12,7 +13,7 @@ export interface KeyValuePair { } export const flattenObject = ( - item: Record | null | undefined, + item: Maybe>, parentKey?: string ): KeyValuePair[] => { if (item) { diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters.ts b/x-pack/legacy/plugins/apm/public/utils/formatters.ts deleted file mode 100644 index 34b552230fa77a..00000000000000 --- a/x-pack/legacy/plugins/apm/public/utils/formatters.ts +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import numeral from '@elastic/numeral'; -import { i18n } from '@kbn/i18n'; -import { memoize } from 'lodash'; -import { NOT_AVAILABLE_LABEL } from '../../common/i18n'; - -const HOURS_CUT_OFF = 3600000000; // 1 hour (in microseconds) -const MINUTES_CUT_OFF = 60000000; // 1 minute (in microseconds) -const SECONDS_CUT_OFF = 10 * 1000000; // 10 seconds (in microseconds) -const MILLISECONDS_CUT_OFF = 10 * 1000; // 10 milliseconds (in microseconds) -const SPACE = ' '; - -/* - * value: time in microseconds - * withUnit: add unit suffix - * defaultValue: value to use if the specified is null/undefined - */ -type FormatterValue = number | undefined | null; -interface FormatterOptions { - withUnit?: boolean; - defaultValue?: string; -} - -export function asHours( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const hoursLabel = - SPACE + - i18n.translate('xpack.apm.formatters.hoursTimeUnitLabel', { - defaultMessage: 'h' - }); - const formatted = asDecimal(value / 3600000000); - return `${formatted}${withUnit ? hoursLabel : ''}`; -} - -export function asMinutes( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const minutesLabel = - SPACE + - i18n.translate('xpack.apm.formatters.minutesTimeUnitLabel', { - defaultMessage: 'min' - }); - const formatted = asDecimal(value / 60000000); - return `${formatted}${withUnit ? minutesLabel : ''}`; -} - -export function asSeconds( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const secondsLabel = - SPACE + - i18n.translate('xpack.apm.formatters.secondsTimeUnitLabel', { - defaultMessage: 's' - }); - const formatted = asDecimal(value / 1000000); - return `${formatted}${withUnit ? secondsLabel : ''}`; -} - -export function asMillis( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - - const millisLabel = - SPACE + - i18n.translate('xpack.apm.formatters.millisTimeUnitLabel', { - defaultMessage: 'ms' - }); - const formatted = asInteger(value / 1000); - return `${formatted}${withUnit ? millisLabel : ''}`; -} - -export function asMicros( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - - const microsLabel = - SPACE + - i18n.translate('xpack.apm.formatters.microsTimeUnitLabel', { - defaultMessage: 'μs' - }); - const formatted = asInteger(value); - return `${formatted}${withUnit ? microsLabel : ''}`; -} - -export type TimeFormatter = ( - value: FormatterValue, - options?: FormatterOptions -) => string; - -type TimeFormatterBuilder = (max: number) => TimeFormatter; - -export const getTimeFormatter: TimeFormatterBuilder = memoize((max: number) => { - const unit = timeUnit(max); - switch (unit) { - case 'h': - return asHours; - case 'm': - return asMinutes; - case 's': - return asSeconds; - case 'ms': - return asMillis; - case 'us': - return asMicros; - } -}); - -export function timeUnit(max: number) { - if (max > HOURS_CUT_OFF) { - return 'h'; - } else if (max > MINUTES_CUT_OFF) { - return 'm'; - } else if (max > SECONDS_CUT_OFF) { - return 's'; - } else if (max > MILLISECONDS_CUT_OFF) { - return 'ms'; - } else { - return 'us'; - } -} - -export function asTime( - value: FormatterValue, - { withUnit = true, defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} -) { - if (value == null) { - return defaultValue; - } - const formatter = getTimeFormatter(value); - return formatter(value, { withUnit, defaultValue }); -} - -export function asDecimal(value: number) { - return numeral(value).format('0,0.0'); -} - -export function asInteger(value: number) { - return numeral(value).format('0,0'); -} - -export function tpmUnit(type?: string) { - return type === 'request' - ? i18n.translate('xpack.apm.formatters.requestsPerMinLabel', { - defaultMessage: 'rpm' - }) - : i18n.translate('xpack.apm.formatters.transactionsPerMinLabel', { - defaultMessage: 'tpm' - }); -} - -export function asPercent( - numerator: number, - denominator: number | undefined, - fallbackResult = '' -) { - if (!denominator || isNaN(numerator)) { - return fallbackResult; - } - - const decimal = numerator / denominator; - return numeral(decimal).format('0.0%'); -} - -function asKilobytes(value: number) { - return `${asDecimal(value / 1000)} KB`; -} - -function asMegabytes(value: number) { - return `${asDecimal(value / 1e6)} MB`; -} - -function asGigabytes(value: number) { - return `${asDecimal(value / 1e9)} GB`; -} - -function asTerabytes(value: number) { - return `${asDecimal(value / 1e12)} TB`; -} - -function asBytes(value: number) { - return `${asDecimal(value)} B`; -} - -const bailIfNumberInvalid = (cb: (val: number) => string) => { - return (val: number | null | undefined) => { - if (val === null || val === undefined || isNaN(val)) { - return ''; - } - return cb(val); - }; -}; - -export const asDynamicBytes = bailIfNumberInvalid((value: number) => { - return unmemoizedFixedByteFormatter(value)(value); -}); - -const unmemoizedFixedByteFormatter = (max: number) => { - if (max > 1e12) { - return asTerabytes; - } - - if (max > 1e9) { - return asGigabytes; - } - - if (max > 1e6) { - return asMegabytes; - } - - if (max > 1000) { - return asKilobytes; - } - - return asBytes; -}; - -export const getFixedByteFormatter = memoize((max: number) => { - const formatter = unmemoizedFixedByteFormatter(max); - - return bailIfNumberInvalid(formatter); -}); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts new file mode 100644 index 00000000000000..bec9cede00a2be --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment from 'moment-timezone'; +import { asRelativeDateTimeRange, asAbsoluteDateTime } from '../datetime'; + +describe('date time formatters', () => { + describe('asRelativeDateTimeRange', () => { + beforeAll(() => { + moment.tz.setDefault('Europe/Amsterdam'); + }); + afterAll(() => moment.tz.setDefault('')); + const formatDateToTimezone = (dateTimeString: string) => + moment(dateTimeString).valueOf(); + + describe('YYYY - YYYY', () => { + it('range: 10 years', () => { + const start = formatDateToTimezone('2000-01-01 10:01:01'); + const end = formatDateToTimezone('2010-01-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('2000 - 2010'); + }); + it('range: 5 years', () => { + const start = formatDateToTimezone('2010-01-01 10:01:01'); + const end = formatDateToTimezone('2015-01-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('2010 - 2015'); + }); + }); + describe('MMM YYYY - MMM YYYY', () => { + it('range: 4 years ', () => { + const start = formatDateToTimezone('2010-01-01 10:01:01'); + const end = formatDateToTimezone('2014-04-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Jan 2010 - Apr 2014'); + }); + it('range: 6 months ', () => { + const start = formatDateToTimezone('2019-01-01 10:01:01'); + const end = formatDateToTimezone('2019-07-01 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Jan 2019 - Jul 2019'); + }); + }); + describe('MMM D, YYYY - MMM D, YYYY', () => { + it('range: 2 days', () => { + const start = formatDateToTimezone('2019-10-01 10:01:01'); + const end = formatDateToTimezone('2019-10-05 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 1, 2019 - Oct 5, 2019'); + }); + it('range: 1 day', () => { + const start = formatDateToTimezone('2019-10-01 10:01:01'); + const end = formatDateToTimezone('2019-10-03 10:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 1, 2019 - Oct 3, 2019'); + }); + }); + describe('MMM D, YYYY, HH:mm - HH:mm (UTC)', () => { + it('range: 9 hours', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 19:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01 - 19:01 (UTC+1)'); + }); + it('range: 5 hours', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 15:01:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01 - 15:01 (UTC+1)'); + }); + }); + describe('MMM D, YYYY, HH:mm:ss - HH:mm:ss (UTC)', () => { + it('range: 14 minutes', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 10:15:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01:01 - 10:15:01 (UTC+1)'); + }); + it('range: 5 minutes', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01'); + const end = formatDateToTimezone('2019-10-29 10:06:01'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual('Oct 29, 2019, 10:01:01 - 10:06:01 (UTC+1)'); + }); + }); + describe('MMM D, YYYY, HH:mm:ss.SSS - HH:mm:ss.SSS (UTC)', () => { + it('range: 9 seconds', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01.001'); + const end = formatDateToTimezone('2019-10-29 10:01:10.002'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual( + 'Oct 29, 2019, 10:01:01.001 - 10:01:10.002 (UTC+1)' + ); + }); + it('range: 1 second', () => { + const start = formatDateToTimezone('2019-10-29 10:01:01.001'); + const end = formatDateToTimezone('2019-10-29 10:01:02.002'); + const dateRange = asRelativeDateTimeRange(start, end); + expect(dateRange).toEqual( + 'Oct 29, 2019, 10:01:01.001 - 10:01:02.002 (UTC+1)' + ); + }); + }); + }); + + describe('asAbsoluteDateTime', () => { + afterAll(() => moment.tz.setDefault('')); + + it('should add a leading plus for timezones with positive UTC offset', () => { + moment.tz.setDefault('Europe/Copenhagen'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 14:00 (UTC+2)' + ); + }); + + it('should add a leading minus for timezones with negative UTC offset', () => { + moment.tz.setDefault('America/Los_Angeles'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 05:00 (UTC-7)' + ); + }); + + it('should use default UTC offset formatting when offset contains minutes', () => { + moment.tz.setDefault('Canada/Newfoundland'); + expect(asAbsoluteDateTime(1559390400000, 'minutes')).toBe( + 'Jun 1, 2019, 09:30 (UTC-02:30)' + ); + }); + + it('should respect DST', () => { + moment.tz.setDefault('Europe/Copenhagen'); + const timeWithDST = 1559390400000; // Jun 1, 2019 + const timeWithoutDST = 1575201600000; // Dec 1, 2019 + + expect(asAbsoluteDateTime(timeWithDST)).toBe( + 'Jun 1, 2019, 14:00:00.000 (UTC+2)' + ); + + expect(asAbsoluteDateTime(timeWithoutDST)).toBe( + 'Dec 1, 2019, 13:00:00.000 (UTC+1)' + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts new file mode 100644 index 00000000000000..014ecad01d4d70 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { asDuration, convertTo, toMicroseconds } from '../duration'; + +describe('duration formatters', () => { + describe('asDuration', () => { + it('formats correctly with defaults', () => { + expect(asDuration(null)).toEqual('N/A'); + expect(asDuration(undefined)).toEqual('N/A'); + expect(asDuration(0)).toEqual('0 μs'); + expect(asDuration(1)).toEqual('1 μs'); + expect(asDuration(toMicroseconds(1, 'milliseconds'))).toEqual('1,000 μs'); + expect(asDuration(toMicroseconds(1000, 'milliseconds'))).toEqual( + '1,000 ms' + ); + expect(asDuration(toMicroseconds(10000, 'milliseconds'))).toEqual( + '10,000 ms' + ); + expect(asDuration(toMicroseconds(20, 'seconds'))).toEqual('20.0 s'); + expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('10.0 min'); + expect(asDuration(toMicroseconds(1, 'hours'))).toEqual('60.0 min'); + expect(asDuration(toMicroseconds(1.5, 'hours'))).toEqual('1.5 h'); + }); + + it('falls back to default value', () => { + expect(asDuration(undefined, { defaultValue: 'nope' })).toEqual('nope'); + }); + }); + + describe('convertTo', () => { + it('hours', () => { + const unit = 'hours'; + const oneHourAsMicro = toMicroseconds(1, 'hours'); + const twoHourAsMicro = toMicroseconds(2, 'hours'); + expect(convertTo({ unit, microseconds: oneHourAsMicro })).toEqual({ + unit: 'h', + value: '1.0', + formatted: '1.0 h' + }); + expect(convertTo({ unit, microseconds: twoHourAsMicro })).toEqual({ + unit: 'h', + value: '2.0', + formatted: '2.0 h' + }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '1.2' }) + ).toEqual({ value: '1.2', formatted: '1.2' }); + }); + + it('minutes', () => { + const unit = 'minutes'; + const oneHourAsMicro = toMicroseconds(1, 'hours'); + const twoHourAsMicro = toMicroseconds(2, 'hours'); + expect(convertTo({ unit, microseconds: oneHourAsMicro })).toEqual({ + unit: 'min', + value: '60.0', + formatted: '60.0 min' + }); + expect(convertTo({ unit, microseconds: twoHourAsMicro })).toEqual({ + unit: 'min', + value: '120.0', + formatted: '120.0 min' + }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + + it('seconds', () => { + const unit = 'seconds'; + const twentySecondsAsMicro = toMicroseconds(20, 'seconds'); + const thirtyFiveSecondsAsMicro = toMicroseconds(35, 'seconds'); + expect(convertTo({ unit, microseconds: twentySecondsAsMicro })).toEqual({ + unit: 's', + value: '20.0', + formatted: '20.0 s' + }); + expect( + convertTo({ unit, microseconds: thirtyFiveSecondsAsMicro }) + ).toEqual({ unit: 's', value: '35.0', formatted: '35.0 s' }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + + it('milliseconds', () => { + const unit = 'milliseconds'; + const twentyMilliAsMicro = toMicroseconds(20, 'milliseconds'); + const thirtyFiveMilliAsMicro = toMicroseconds(35, 'milliseconds'); + expect(convertTo({ unit, microseconds: twentyMilliAsMicro })).toEqual({ + unit: 'ms', + value: '20', + formatted: '20 ms' + }); + expect( + convertTo({ unit, microseconds: thirtyFiveMilliAsMicro }) + ).toEqual({ unit: 'ms', value: '35', formatted: '35 ms' }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + + it('microseconds', () => { + const unit = 'microseconds'; + expect(convertTo({ unit, microseconds: 20 })).toEqual({ + unit: 'μs', + value: '20', + formatted: '20 μs' + }); + expect(convertTo({ unit, microseconds: 35 })).toEqual({ + unit: 'μs', + value: '35', + formatted: '35 μs' + }); + expect( + convertTo({ unit, microseconds: null, defaultValue: '10' }) + ).toEqual({ value: '10', formatted: '10' }); + }); + }); + describe('toMicroseconds', () => { + it('transformes to microseconds', () => { + expect(toMicroseconds(1, 'hours')).toEqual(3600000000); + expect(toMicroseconds(10, 'minutes')).toEqual(600000000); + expect(toMicroseconds(10, 'seconds')).toEqual(10000000); + expect(toMicroseconds(10, 'milliseconds')).toEqual(10000); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts new file mode 100644 index 00000000000000..f6ed88a850a5b2 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { asPercent } from '../formatters'; + +describe('formatters', () => { + describe('asPercent', () => { + it('should divide and format item as percent', () => { + expect(asPercent(3725, 10000, 'n/a')).toEqual('37.3%'); + }); + + it('should format when numerator is 0', () => { + expect(asPercent(0, 1, 'n/a')).toEqual('0.0%'); + }); + + it('should return fallback when denominator is undefined', () => { + expect(asPercent(3725, undefined, 'n/a')).toEqual('n/a'); + }); + + it('should return fallback when denominator is 0 ', () => { + expect(asPercent(3725, 0, 'n/a')).toEqual('n/a'); + }); + + it('should return fallback when numerator or denominator is NaN', () => { + expect(asPercent(3725, NaN, 'n/a')).toEqual('n/a'); + expect(asPercent(NaN, 10000, 'n/a')).toEqual('n/a'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts similarity index 63% rename from x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts rename to x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts index 093624240565f0..07d3d0c1eb08f6 100644 --- a/x-pack/legacy/plugins/apm/public/utils/__test__/formatters.test.ts +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts @@ -3,61 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { getFixedByteFormatter, asDynamicBytes } from '../size'; -import { - asPercent, - asTime, - getFixedByteFormatter, - asDynamicBytes -} from '../formatters'; - -describe('formatters', () => { - describe('asTime', () => { - it('formats correctly with defaults', () => { - expect(asTime(null)).toEqual('N/A'); - expect(asTime(undefined)).toEqual('N/A'); - expect(asTime(0)).toEqual('0 μs'); - expect(asTime(1)).toEqual('1 μs'); - expect(asTime(1000)).toEqual('1,000 μs'); - expect(asTime(1000 * 1000)).toEqual('1,000 ms'); - expect(asTime(1000 * 1000 * 10)).toEqual('10,000 ms'); - expect(asTime(1000 * 1000 * 20)).toEqual('20.0 s'); - expect(asTime(60000000 * 10)).toEqual('10.0 min'); - expect(asTime(3600000000 * 1.5)).toEqual('1.5 h'); - }); - - it('formats without unit', () => { - expect(asTime(1000, { withUnit: false })).toEqual('1,000'); - }); - - it('falls back to default value', () => { - expect(asTime(undefined, { defaultValue: 'nope' })).toEqual('nope'); - }); - }); - - describe('asPercent', () => { - it('should divide and format item as percent', () => { - expect(asPercent(3725, 10000, 'n/a')).toEqual('37.3%'); - }); - - it('should format when numerator is 0', () => { - expect(asPercent(0, 1, 'n/a')).toEqual('0.0%'); - }); - - it('should return fallback when denominator is undefined', () => { - expect(asPercent(3725, undefined, 'n/a')).toEqual('n/a'); - }); - - it('should return fallback when denominator is 0 ', () => { - expect(asPercent(3725, 0, 'n/a')).toEqual('n/a'); - }); - - it('should return fallback when numerator or denominator is NaN', () => { - expect(asPercent(3725, NaN, 'n/a')).toEqual('n/a'); - expect(asPercent(NaN, 10000, 'n/a')).toEqual('n/a'); - }); - }); - +describe('size formatters', () => { describe('byte formatting', () => { const bytes = 10; const kb = 1000 + 1; diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts new file mode 100644 index 00000000000000..98483a0351f069 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment-timezone'; + +/** + * Returns the timezone set on momentTime. + * (UTC+offset) when offset if bigger than 0. + * (UTC-offset) when offset if lower than 0. + * @param momentTime Moment + */ +function formatTimezone(momentTime: moment.Moment) { + const DEFAULT_TIMEZONE_FORMAT = 'Z'; + + const utcOffsetHours = momentTime.utcOffset() / 60; + + const customTimezoneFormat = + utcOffsetHours > 0 ? `+${utcOffsetHours}` : utcOffsetHours; + + const utcOffsetFormatted = Number.isInteger(utcOffsetHours) + ? customTimezoneFormat + : DEFAULT_TIMEZONE_FORMAT; + + return momentTime.format(`(UTC${utcOffsetFormatted})`); +} + +export type TimeUnit = 'hours' | 'minutes' | 'seconds' | 'milliseconds'; +function getTimeFormat(timeUnit: TimeUnit) { + switch (timeUnit) { + case 'hours': + return 'HH'; + case 'minutes': + return 'HH:mm'; + case 'seconds': + return 'HH:mm:ss'; + case 'milliseconds': + return 'HH:mm:ss.SSS'; + default: + return ''; + } +} + +type DateUnit = 'days' | 'months' | 'years'; +function getDateFormat(dateUnit: DateUnit) { + switch (dateUnit) { + case 'years': + return 'YYYY'; + case 'months': + return 'MMM YYYY'; + case 'days': + return 'MMM D, YYYY'; + default: + return ''; + } +} + +function getFormatsAccordingToDateDifference( + momentStart: moment.Moment, + momentEnd: moment.Moment +) { + const getDateDifference = (unitOfTime: DateUnit | TimeUnit) => + momentEnd.diff(momentStart, unitOfTime); + + if (getDateDifference('years') >= 5) { + return { dateFormat: getDateFormat('years') }; + } + + if (getDateDifference('months') >= 5) { + return { dateFormat: getDateFormat('months') }; + } + + const dateFormatWithDays = getDateFormat('days'); + if (getDateDifference('days') > 1) { + return { dateFormat: dateFormatWithDays }; + } + + if (getDateDifference('hours') >= 5) { + return { + dateFormat: dateFormatWithDays, + timeFormat: getTimeFormat('minutes') + }; + } + + if (getDateDifference('minutes') >= 5) { + return { + dateFormat: dateFormatWithDays, + timeFormat: getTimeFormat('seconds') + }; + } + + return { + dateFormat: dateFormatWithDays, + timeFormat: getTimeFormat('milliseconds') + }; +} + +export function asAbsoluteDateTime( + time: number, + timeUnit: TimeUnit = 'milliseconds' +) { + const momentTime = moment(time); + const formattedTz = formatTimezone(momentTime); + + return momentTime.format( + `${getDateFormat('days')}, ${getTimeFormat(timeUnit)} ${formattedTz}` + ); +} + +/** + * + * Returns the dates formatted according to the difference between the two dates: + * + * | Difference | Format | + * | -------------- |:----------------------------------------------:| + * | >= 5 years | YYYY - YYYY | + * | >= 5 months | MMM YYYY - MMM YYYY | + * | > 1 day | MMM D, YYYY - MMM D, YYYY | + * | >= 5 hours | MMM D, YYYY, HH:mm - HH:mm (UTC) | + * | >= 5 minutes | MMM D, YYYY, HH:mm:ss - HH:mm:ss (UTC) | + * | default | MMM D, YYYY, HH:mm:ss.SSS - HH:mm:ss.SSS (UTC) | + * + * @param start timestamp + * @param end timestamp + */ +export function asRelativeDateTimeRange(start: number, end: number) { + const momentStartTime = moment(start); + const momentEndTime = moment(end); + + const { dateFormat, timeFormat } = getFormatsAccordingToDateDifference( + momentStartTime, + momentEndTime + ); + + if (timeFormat) { + const startFormatted = momentStartTime.format( + `${dateFormat}, ${timeFormat}` + ); + const endFormatted = momentEndTime.format(timeFormat); + const formattedTz = formatTimezone(momentStartTime); + return `${startFormatted} - ${endFormatted} ${formattedTz}`; + } + + const startFormatted = momentStartTime.format(dateFormat); + const endFormatted = momentEndTime.format(dateFormat); + return `${startFormatted} - ${endFormatted}`; +} diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts new file mode 100644 index 00000000000000..39341e1ff4443b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { memoize } from 'lodash'; +import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; +import { asDecimal, asInteger } from './formatters'; +import { TimeUnit } from './datetime'; +import { Maybe } from '../../../typings/common'; + +interface FormatterOptions { + defaultValue?: string; +} + +type DurationTimeUnit = TimeUnit | 'microseconds'; + +interface DurationUnit { + [unit: string]: { + label: string; + convert: (value: number) => string; + }; +} + +interface ConvertedDuration { + value: string; + unit?: string; + formatted: string; +} + +export type TimeFormatter = ( + value: Maybe, + options?: FormatterOptions +) => ConvertedDuration; + +type TimeFormatterBuilder = (max: number) => TimeFormatter; + +const durationUnit: DurationUnit = { + hours: { + label: i18n.translate('xpack.apm.formatters.hoursTimeUnitLabel', { + defaultMessage: 'h' + }), + convert: (value: number) => + asDecimal(moment.duration(value / 1000).asHours()) + }, + minutes: { + label: i18n.translate('xpack.apm.formatters.minutesTimeUnitLabel', { + defaultMessage: 'min' + }), + convert: (value: number) => + asDecimal(moment.duration(value / 1000).asMinutes()) + }, + seconds: { + label: i18n.translate('xpack.apm.formatters.secondsTimeUnitLabel', { + defaultMessage: 's' + }), + convert: (value: number) => + asDecimal(moment.duration(value / 1000).asSeconds()) + }, + milliseconds: { + label: i18n.translate('xpack.apm.formatters.millisTimeUnitLabel', { + defaultMessage: 'ms' + }), + convert: (value: number) => + asInteger(moment.duration(value / 1000).asMilliseconds()) + }, + microseconds: { + label: i18n.translate('xpack.apm.formatters.microsTimeUnitLabel', { + defaultMessage: 'μs' + }), + convert: (value: number) => asInteger(value) + } +}; + +/** + * Converts a microseconds value into the unit defined. + * + * @param param0 + * { unit: "milliseconds" | "hours" | "minutes" | "seconds" | "microseconds", microseconds, defaultValue } + * + * @returns object { value, unit, formatted } + */ +export function convertTo({ + unit, + microseconds, + defaultValue = NOT_AVAILABLE_LABEL +}: { + unit: DurationTimeUnit; + microseconds: Maybe; + defaultValue?: string; +}): ConvertedDuration { + const duration = durationUnit[unit]; + if (!duration || microseconds == null) { + return { value: defaultValue, formatted: defaultValue }; + } + + const convertedValue = duration.convert(microseconds); + return { + value: convertedValue, + unit: duration.label, + formatted: `${convertedValue} ${duration.label}` + }; +} + +export const toMicroseconds = (value: number, timeUnit: TimeUnit) => + moment.duration(value, timeUnit).asMilliseconds() * 1000; + +function getDurationUnitKey(max: number): DurationTimeUnit { + if (max > toMicroseconds(1, 'hours')) { + return 'hours'; + } + if (max > toMicroseconds(1, 'minutes')) { + return 'minutes'; + } + if (max > toMicroseconds(10, 'seconds')) { + return 'seconds'; + } + if (max > toMicroseconds(10, 'milliseconds')) { + return 'milliseconds'; + } + return 'microseconds'; +} + +export const getDurationFormatter: TimeFormatterBuilder = memoize( + (max: number) => { + const unit = getDurationUnitKey(max); + return (value, { defaultValue }: FormatterOptions = {}) => { + return convertTo({ unit, microseconds: value, defaultValue }); + }; + } +); + +/** + * Converts value and returns it formatted - 00 unit + * + * @param value + * @param param1 { defaultValue } + * @returns formated value - 00 unit + */ +export function asDuration( + value: Maybe, + { defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {} +) { + if (value == null) { + return defaultValue; + } + + const formatter = getDurationFormatter(value); + return formatter(value, { defaultValue }).formatted; +} diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts new file mode 100644 index 00000000000000..630b6a0a18dbf7 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.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 numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; + +export function asDecimal(value: number) { + return numeral(value).format('0,0.0'); +} + +export function asInteger(value: number) { + return numeral(value).format('0,0'); +} + +export function tpmUnit(type?: string) { + return type === 'request' + ? i18n.translate('xpack.apm.formatters.requestsPerMinLabel', { + defaultMessage: 'rpm' + }) + : i18n.translate('xpack.apm.formatters.transactionsPerMinLabel', { + defaultMessage: 'tpm' + }); +} + +export function asPercent( + numerator: number, + denominator: number | undefined, + fallbackResult = '' +) { + if (!denominator || isNaN(numerator)) { + return fallbackResult; + } + + const decimal = numerator / denominator; + return numeral(decimal).format('0.0%'); +} diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts new file mode 100644 index 00000000000000..4fedd55ff1e893 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './formatters'; +export * from './datetime'; +export * from './duration'; +export * from './size'; diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts new file mode 100644 index 00000000000000..2cdf8af1d46de8 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { memoize } from 'lodash'; +import { asDecimal } from './formatters'; +import { Maybe } from '../../../typings/common'; + +function asKilobytes(value: number) { + return `${asDecimal(value / 1000)} KB`; +} + +function asMegabytes(value: number) { + return `${asDecimal(value / 1e6)} MB`; +} + +function asGigabytes(value: number) { + return `${asDecimal(value / 1e9)} GB`; +} + +function asTerabytes(value: number) { + return `${asDecimal(value / 1e12)} TB`; +} + +function asBytes(value: number) { + return `${asDecimal(value)} B`; +} + +const bailIfNumberInvalid = (cb: (val: number) => string) => { + return (val: Maybe) => { + if (val === null || val === undefined || isNaN(val)) { + return ''; + } + return cb(val); + }; +}; + +export const getFixedByteFormatter = memoize((max: number) => { + const formatter = unmemoizedFixedByteFormatter(max); + + return bailIfNumberInvalid(formatter); +}); + +export const asDynamicBytes = bailIfNumberInvalid((value: number) => { + return unmemoizedFixedByteFormatter(value)(value); +}); + +const unmemoizedFixedByteFormatter = (max: number) => { + if (max > 1e12) { + return asTerabytes; + } + + if (max > 1e9) { + return asGigabytes; + } + + if (max > 1e6) { + return asMegabytes; + } + + if (max > 1000) { + return asKilobytes; + } + + return asBytes; +}; diff --git a/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts b/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts index 411d03fce349d6..c36efc232b7824 100644 --- a/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts +++ b/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Maybe } from '../../typings/common'; -export const isValidCoordinateValue = ( - value: number | null | undefined -): value is number => value !== null && value !== undefined; +export const isValidCoordinateValue = (value: Maybe): value is number => + value !== null && value !== undefined; diff --git a/x-pack/legacy/plugins/apm/typings/common.d.ts b/x-pack/legacy/plugins/apm/typings/common.d.ts index d79b05ed99b496..b9064980bd6577 100644 --- a/x-pack/legacy/plugins/apm/typings/common.d.ts +++ b/x-pack/legacy/plugins/apm/typings/common.d.ts @@ -27,3 +27,5 @@ export type PromiseReturnType = Func extends ( ) => Promise ? Value : Func; + +export type Maybe = T | null | undefined; diff --git a/x-pack/legacy/plugins/apm/typings/timeseries.ts b/x-pack/legacy/plugins/apm/typings/timeseries.ts index 9b9f7dcc2c8207..d64486d8e71e91 100644 --- a/x-pack/legacy/plugins/apm/typings/timeseries.ts +++ b/x-pack/legacy/plugins/apm/typings/timeseries.ts @@ -3,10 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Maybe } from '../typings/common'; export interface Coordinate { x: number; - y: number | null | undefined; + y: Maybe; } export interface RectCoordinate { From e064205471590fe10c06feefda0b5caae4de2109 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Fri, 15 Nov 2019 13:56:56 +0100 Subject: [PATCH 20/28] update chromedriver to 78 (#50737) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c0e2acdb578c49..3abd69a6166829 100644 --- a/package.json +++ b/package.json @@ -360,7 +360,7 @@ "chance": "1.0.18", "cheerio": "0.22.0", "chokidar": "3.2.1", - "chromedriver": "^77.0.0", + "chromedriver": "78.0.1", "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index 5bb6bb3da2e47c..63726faf0adb35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7737,10 +7737,10 @@ chrome-trace-event@^1.0.0, chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^77.0.0: - version "77.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-77.0.0.tgz#bd916cc87a0ccb7a6e4fb4b43cb2368bc54db6a0" - integrity sha512-mZa1IVx4HD8rDaItWbnS470mmypgiWsDiu98r0NkiT4uLm3qrANl4vOU6no6vtWtLQiW5kt1POcIbjeNpsLbXA== +chromedriver@78.0.1: + version "78.0.1" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-78.0.1.tgz#2db3425a2cba6fcaf1a41d9538b16c3d06fa74a8" + integrity sha512-eOsyFk4xb9EECs1VMrDbxO713qN+Bu1XUE8K9AuePc3839TPdAegg72kpXSzkeNqRNZiHbnJUItIVCLFkDqceA== dependencies: del "^4.1.1" extract-zip "^1.6.7" From 63a70c0d2ad9d6d6dbf169be629f4c83b3b906fc Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 15 Nov 2019 15:19:02 +0100 Subject: [PATCH 21/28] [Grokdebugger] Theme and Mode imports (#50473) * Added textmate theme and JSON mode imports * Added text themes to other inputs [skip ci] --- .../custom_patterns_input/custom_patterns_input.js | 2 ++ .../public/components/event_input/event_input.js | 2 ++ .../public/components/event_output/event_output.js | 1 + .../public/components/grok_debugger/brace_imports.ts | 9 +++++++++ .../public/components/grok_debugger/grok_debugger.js | 3 +++ .../public/components/pattern_input/pattern_input.js | 1 + 6 files changed, 18 insertions(+) create mode 100644 x-pack/legacy/plugins/grokdebugger/public/components/grok_debugger/brace_imports.ts diff --git a/x-pack/legacy/plugins/grokdebugger/public/components/custom_patterns_input/custom_patterns_input.js b/x-pack/legacy/plugins/grokdebugger/public/components/custom_patterns_input/custom_patterns_input.js index cc1d2bc53a9df6..7f0b25e7e25af8 100644 --- a/x-pack/legacy/plugins/grokdebugger/public/components/custom_patterns_input/custom_patterns_input.js +++ b/x-pack/legacy/plugins/grokdebugger/public/components/custom_patterns_input/custom_patterns_input.js @@ -57,6 +57,8 @@ MSG message-id=<%{GREEDYDATA}>`; Date: Fri, 15 Nov 2019 15:15:13 +0000 Subject: [PATCH 22/28] Adding tooltip explanation on range filters (#50186) --- .../vis/editors/default/controls/ranges.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/legacy/ui/public/vis/editors/default/controls/ranges.tsx b/src/legacy/ui/public/vis/editors/default/controls/ranges.tsx index 071e15f8b97f83..a216ad5d928b6d 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/ranges.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/ranges.tsx @@ -29,6 +29,8 @@ import { EuiSpacer, EuiButtonEmpty, EuiFormRow, + EuiToolTip, + EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -154,15 +156,24 @@ function RangesParamEditor({ [isFromValid, isToValid] = validateRange({ from, to }, index); } - const fromPrepend = i18n.translate( + const gtePrependLabel = i18n.translate( 'common.ui.aggTypes.ranges.greaterThanOrEqualPrepend', { defaultMessage: '\u2265', } ); - const toPrepend = i18n.translate('common.ui.aggTypes.ranges.lessThanPrepend', { + const gteTooltipContent = i18n.translate( + 'common.ui.aggTypes.ranges.greaterThanOrEqualTooltip', + { + defaultMessage: 'Greater than or equal to', + } + ); + const ltPrependLabel = i18n.translate('common.ui.aggTypes.ranges.lessThanPrepend', { defaultMessage: '\u003c', }); + const ltTooltipContent = i18n.translate('common.ui.aggTypes.ranges.lessThanTooltip', { + defaultMessage: 'Less than', + }); return ( @@ -179,7 +190,11 @@ function RangesParamEditor({ fullWidth={true} compressed={true} isInvalid={!isFromValid} - prepend={fromPrepend} + prepend={ + + {gtePrependLabel} + + } /> @@ -197,7 +212,11 @@ function RangesParamEditor({ fullWidth={true} compressed={true} isInvalid={!isToValid} - prepend={toPrepend} + prepend={ + + {ltPrependLabel} + + } /> From ae413abfa0428092ff259125b68dbb0ffaa0873a Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 13 Nov 2019 16:13:49 -0800 Subject: [PATCH 23/28] Skip flaky " can navigate Autoplay Settings, closes" test Signed-off-by: Tyler Smalley --- .../components/footer/settings/__tests__/settings.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx index 6957e8599c0fd2..d5963a936fa70d 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx @@ -71,7 +71,7 @@ describe('', () => { expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot(); }); - test('can navigate Toolbar Settings, closes when activated', async () => { + test.skip('can navigate Toolbar Settings, closes when activated', async () => { trigger(wrapper).simulate('click'); expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot(); menuItems(wrapper) From 1dfc70fe6828532337a1fef15cae2c4a90603c7b Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 13 Nov 2019 14:25:14 -0800 Subject: [PATCH 24/28] Skip flaky " can navigate Autoplay Settings" test Signed-off-by: Tyler Smalley --- .../components/footer/settings/__tests__/settings.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx index d5963a936fa70d..0667674b6a7ddb 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/settings.test.tsx @@ -60,7 +60,7 @@ describe('', () => { expect(popover(wrapper).prop('isOpen')).toEqual(false); }); - test('can navigate Autoplay Settings', async () => { + test.skip('can navigate Autoplay Settings', async () => { trigger(wrapper).simulate('click'); expect(takeMountedSnapshot(portal(wrapper))).toMatchSnapshot(); await tick(20); From 1c415e0cadd46a871de12ca42c066f23c44ea128 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Fri, 15 Nov 2019 10:53:33 -0500 Subject: [PATCH 25/28] Default payload validation (#48753) * trial for default payload validation * relaxing default validation * some cleanup and testing * update xsrf integration test * adding API smoke tests * fixing types * removing Joi extensions * updating tests * documenting changes * fixing NP validation bypass * fix lint problems * Update src/legacy/server/http/integration_tests/xsrf.test.js * Update src/legacy/server/http/integration_tests/xsrf.test.js * revert test changes * simplifying tests Co-authored-by: Elastic Machine --- .../server/http/base_path_proxy_server.ts | 2 + src/core/server/http/http_server.ts | 7 ++ src/core/server/http/http_tools.ts | 6 ++ .../validate_object.test.ts.snap | 13 +++ .../server/http/prototype_pollution/index.ts | 20 +++++ .../validate_object.test.ts | 79 ++++++++++++++++++ .../prototype_pollution/validate_object.ts | 80 +++++++++++++++++++ .../console/server/proxy_route.js | 1 + .../http/integration_tests/xsrf.test.js | 6 +- test/api_integration/apis/general/index.js | 1 + .../apis/general/prototype_pollution.ts | 57 +++++++++++++ .../api_integration/ftr_provider_context.d.ts | 24 ++++++ 12 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap create mode 100644 src/core/server/http/prototype_pollution/index.ts create mode 100644 src/core/server/http/prototype_pollution/validate_object.test.ts create mode 100644 src/core/server/http/prototype_pollution/validate_object.ts create mode 100644 test/api_integration/apis/general/prototype_pollution.ts create mode 100644 test/api_integration/ftr_provider_context.d.ts diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index ff7fee0198f681..cde35f3cbe995a 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -143,6 +143,7 @@ export class BasePathProxyServer { return responseToolkit.continue; }, ], + validate: { payload: true }, }, path: `${this.httpConfig.basePath}/{kbnPath*}`, }); @@ -175,6 +176,7 @@ export class BasePathProxyServer { return responseToolkit.continue; }, ], + validate: { payload: true }, }, path: `/__UNSAFE_bypassBasePath/{kbnPath*}`, }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 3354324c12407d..da97ab535516c8 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -128,6 +128,8 @@ export class HttpServer { for (const route of router.getRoutes()) { this.log.debug(`registering route handler for [${route.path}]`); const { authRequired = true, tags } = route.options; + // Hapi does not allow payload validation to be specified for 'head' or 'get' requests + const validate = ['head', 'get'].includes(route.method) ? undefined : { payload: true }; this.server.route({ handler: route.handler, method: route.method, @@ -135,6 +137,11 @@ export class HttpServer { options: { auth: authRequired ? undefined : false, tags: tags ? Array.from(tags) : undefined, + // TODO: This 'validate' section can be removed once the legacy platform is completely removed. + // We are telling Hapi that NP routes can accept any payload, so that it can bypass the default + // validation applied in ./http_tools#getServerOptions + // (All NP routes are already required to specify their own validation in order to access the payload) + validate, }, }); } diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 88164a76c66f07..22468a5b252f42 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -23,6 +23,7 @@ import Hoek from 'hoek'; import { ServerOptions as TLSOptions } from 'https'; import { ValidationError } from 'joi'; import { HttpConfig } from './http_config'; +import { validateObject } from './prototype_pollution'; /** * Converts Kibana `HttpConfig` into `ServerOptions` that are accepted by the Hapi server. @@ -45,6 +46,11 @@ export function getServerOptions(config: HttpConfig, { configureTLS = true } = { options: { abortEarly: false, }, + // TODO: This payload validation can be removed once the legacy platform is completely removed. + // This is a default payload validation which applies to all LP routes which do not specify their own + // `validate.payload` handler, in order to reduce the likelyhood of prototype pollution vulnerabilities. + // (All NP routes are already required to specify their own validation in order to access the payload) + payload: value => Promise.resolve(validateObject(value)), }, }, state: { diff --git a/src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap b/src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap new file mode 100644 index 00000000000000..937e040c771ee1 --- /dev/null +++ b/src/core/server/http/prototype_pollution/__snapshots__/validate_object.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`can't submit {"__proto__":null} 1`] = `"'__proto__' is an invalid key"`; + +exports[`can't submit {"constructor":{"prototype":null}} 1`] = `"'constructor.prototype' is an invalid key"`; + +exports[`can't submit {"foo":{"__proto__":true}} 1`] = `"'__proto__' is an invalid key"`; + +exports[`can't submit {"foo":{"bar":{"__proto__":{}}}} 1`] = `"'__proto__' is an invalid key"`; + +exports[`can't submit {"foo":{"bar":{"constructor":{"prototype":null}}}} 1`] = `"'constructor.prototype' is an invalid key"`; + +exports[`can't submit {"foo":{"constructor":{"prototype":null}}} 1`] = `"'constructor.prototype' is an invalid key"`; diff --git a/src/core/server/http/prototype_pollution/index.ts b/src/core/server/http/prototype_pollution/index.ts new file mode 100644 index 00000000000000..e1a33ffba155e8 --- /dev/null +++ b/src/core/server/http/prototype_pollution/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { validateObject } from './validate_object'; diff --git a/src/core/server/http/prototype_pollution/validate_object.test.ts b/src/core/server/http/prototype_pollution/validate_object.test.ts new file mode 100644 index 00000000000000..9e23d6cec64445 --- /dev/null +++ b/src/core/server/http/prototype_pollution/validate_object.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { validateObject } from './validate_object'; + +test(`fails on circular references`, () => { + const foo: Record = {}; + foo.myself = foo; + + expect(() => + validateObject({ + payload: foo, + }) + ).toThrowErrorMatchingInlineSnapshot(`"circular reference detected"`); +}); + +[ + { + foo: true, + bar: '__proto__', + baz: 1.1, + qux: undefined, + quux: () => null, + quuz: Object.create(null), + }, + { + foo: { + foo: true, + bar: '__proto__', + baz: 1.1, + qux: undefined, + quux: () => null, + quuz: Object.create(null), + }, + }, + { constructor: { foo: { prototype: null } } }, + { prototype: { foo: { constructor: null } } }, +].forEach(value => { + ['headers', 'payload', 'query', 'params'].forEach(property => { + const obj = { + [property]: value, + }; + test(`can submit ${JSON.stringify(obj)}`, () => { + expect(() => validateObject(obj)).not.toThrowError(); + }); + }); +}); + +// if we use the object literal syntax to create the following values, we end up +// actually reassigning the __proto__ which makes it be a non-enumerable not-own property +// which isn't what we want to test here +[ + JSON.parse(`{ "__proto__": null }`), + JSON.parse(`{ "foo": { "__proto__": true } }`), + JSON.parse(`{ "foo": { "bar": { "__proto__": {} } } }`), + JSON.parse(`{ "constructor": { "prototype" : null } }`), + JSON.parse(`{ "foo": { "constructor": { "prototype" : null } } }`), + JSON.parse(`{ "foo": { "bar": { "constructor": { "prototype" : null } } } }`), +].forEach(value => { + test(`can't submit ${JSON.stringify(value)}`, () => { + expect(() => validateObject(value)).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/src/core/server/http/prototype_pollution/validate_object.ts b/src/core/server/http/prototype_pollution/validate_object.ts new file mode 100644 index 00000000000000..cab6ce295ce92a --- /dev/null +++ b/src/core/server/http/prototype_pollution/validate_object.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +interface StackItem { + value: any; + previousKey: string | null; +} + +// we have to do Object.prototype.hasOwnProperty because when you create an object using +// Object.create(null), and I assume other methods, you get an object without a prototype, +// so you can't use current.hasOwnProperty +const hasOwnProperty = (obj: any, property: string) => + Object.prototype.hasOwnProperty.call(obj, property); + +const isObject = (obj: any) => typeof obj === 'object' && obj !== null; + +// we're using a stack instead of recursion so we aren't limited by the call stack +export function validateObject(obj: any) { + if (!isObject(obj)) { + return; + } + + const stack: StackItem[] = [ + { + value: obj, + previousKey: null, + }, + ]; + const seen = new WeakSet([obj]); + + while (stack.length > 0) { + const { value, previousKey } = stack.pop()!; + + if (!isObject(value)) { + continue; + } + + if (hasOwnProperty(value, '__proto__')) { + throw new Error(`'__proto__' is an invalid key`); + } + + if (hasOwnProperty(value, 'prototype') && previousKey === 'constructor') { + throw new Error(`'constructor.prototype' is an invalid key`); + } + + // iterating backwards through an array is reportedly more performant + const entries = Object.entries(value); + for (let i = entries.length - 1; i >= 0; --i) { + const [key, childValue] = entries[i]; + if (isObject(childValue)) { + if (seen.has(childValue)) { + throw new Error('circular reference detected'); + } + + seen.add(childValue); + } + + stack.push({ + value: childValue, + previousKey: key, + }); + } + } +} diff --git a/src/legacy/core_plugins/console/server/proxy_route.js b/src/legacy/core_plugins/console/server/proxy_route.js index 8ce828879a677a..856128f3d4c031 100644 --- a/src/legacy/core_plugins/console/server/proxy_route.js +++ b/src/legacy/core_plugins/console/server/proxy_route.js @@ -71,6 +71,7 @@ export const createProxyRoute = ({ parse: false, }, validate: { + payload: true, query: Joi.object() .keys({ method: Joi.string() diff --git a/src/legacy/server/http/integration_tests/xsrf.test.js b/src/legacy/server/http/integration_tests/xsrf.test.js index 562a94e198631b..baeb61bff6113c 100644 --- a/src/legacy/server/http/integration_tests/xsrf.test.js +++ b/src/legacy/server/http/integration_tests/xsrf.test.js @@ -57,7 +57,8 @@ describe('xsrf request filter', () => { // Disable payload parsing to make HapiJS server accept any content-type header. payload: { parse: false - } + }, + validate: { payload: null } }, handler: async function () { return 'ok'; @@ -71,7 +72,8 @@ describe('xsrf request filter', () => { // Disable payload parsing to make HapiJS server accept any content-type header. payload: { parse: false - } + }, + validate: { payload: null } }, handler: async function () { return 'ok'; diff --git a/test/api_integration/apis/general/index.js b/test/api_integration/apis/general/index.js index 86b7565cba6de0..f8daff1a6e8a89 100644 --- a/test/api_integration/apis/general/index.js +++ b/test/api_integration/apis/general/index.js @@ -21,5 +21,6 @@ export default function ({ loadTestFile }) { describe('general', () => { loadTestFile(require.resolve('./cookies')); loadTestFile(require.resolve('./csp')); + loadTestFile(require.resolve('./prototype_pollution')); }); } diff --git a/test/api_integration/apis/general/prototype_pollution.ts b/test/api_integration/apis/general/prototype_pollution.ts new file mode 100644 index 00000000000000..1b732dc81afa92 --- /dev/null +++ b/test/api_integration/apis/general/prototype_pollution.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from 'test/api_integration/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('prototype pollution smoke test', () => { + it('prevents payloads with the "constructor.prototype" pollution vector from being accepted', async () => { + await supertest + .post('/api/sample_data/some_data_id') + .send([ + { + constructor: { + prototype: 'foo', + }, + }, + ]) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: "'constructor.prototype' is an invalid key", + validation: { source: 'payload', keys: [] }, + }); + }); + + it('prevents payloads with the "__proto__" pollution vector from being accepted', async () => { + await supertest + .post('/api/sample_data/some_data_id') + .send(JSON.parse(`{"foo": { "__proto__": {} } }`)) + .expect(400, { + statusCode: 400, + error: 'Bad Request', + message: "'__proto__' is an invalid key", + validation: { source: 'payload', keys: [] }, + }); + }); + }); +} diff --git a/test/api_integration/ftr_provider_context.d.ts b/test/api_integration/ftr_provider_context.d.ts new file mode 100644 index 00000000000000..60f4914a1d27e1 --- /dev/null +++ b/test/api_integration/ftr_provider_context.d.ts @@ -0,0 +1,24 @@ +/* + * 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; From a32213f849f8b7b4725459e6f475e945328c9356 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 15 Nov 2019 10:55:57 -0500 Subject: [PATCH 26/28] Retry git clone up to 8 times before failing a build (#50734) --- vars/kibanaPipeline.groovy | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index e8d7cc03edad0a..f824acbc63ccda 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -127,7 +127,17 @@ def jobRunner(label, useRamDisk, closure) { } } - def scmVars = checkout scm + def scmVars + + // Try to clone from Github up to 8 times, waiting 15 secs between attempts + retry(8) { + try { + scmVars = checkout scm + } catch (ex) { + sleep 15 + throw ex + } + } withEnv([ "CI=true", From 504604a4f2dc3cd0190f3179e478d5943bc21e73 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 15 Nov 2019 10:59:05 -0500 Subject: [PATCH 27/28] Add labels to shell scripts in Jenkins (#49657) --- .ci/Jenkinsfile_flaky | 10 ++++----- Jenkinsfile | 12 +++++----- vars/kibanaPipeline.groovy | 45 +++++++++++++++++++++++--------------- vars/runbld.groovy | 12 +++++++--- 4 files changed, 47 insertions(+), 32 deletions(-) diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index e1cbac0528b1f9..8ad02b7162b6ab 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -34,7 +34,7 @@ stage("Kibana Pipeline") { if (!IS_XPACK) { kibanaPipeline.buildOss() if (CI_GROUP == '1') { - runbld "./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh" + runbld("./test/scripts/jenkins_build_kbn_tp_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") } } else { kibanaPipeline.buildXpack() @@ -62,18 +62,18 @@ stage("Kibana Pipeline") { def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { if (job == 'firefoxSmoke') { - return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld './test/scripts/jenkins_firefox_smoke.sh' }) + return kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }) } else if(job == 'visualRegression') { - return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld './test/scripts/jenkins_visual_regression.sh' }) + return kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }) } else { return kibanaPipeline.getOssCiGroupWorker(ciGroup) } } if (job == 'firefoxSmoke') { - return kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld './test/scripts/jenkins_xpack_firefox_smoke.sh' }) + return kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') }) } else if(job == 'visualRegression') { - return kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld './test/scripts/jenkins_xpack_visual_regression.sh' }) + return kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld('./test/scripts/jenkins_xpack_visual_regression.sh', 'Execute xpack-visualRegression') }) } else { return kibanaPipeline.getXpackCiGroupWorker(ciGroup) } diff --git a/Jenkinsfile b/Jenkinsfile index 8d8579736f6392..c002832d4d51a3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -24,9 +24,9 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a 'oss-ciGroup10': kibanaPipeline.getOssCiGroupWorker(10), 'oss-ciGroup11': kibanaPipeline.getOssCiGroupWorker(11), 'oss-ciGroup12': kibanaPipeline.getOssCiGroupWorker(12), - 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld './test/scripts/jenkins_firefox_smoke.sh' }), - 'oss-accessibility': kibanaPipeline.getPostBuildWorker('accessibility', { runbld './test/scripts/jenkins_accessibility.sh' }), - 'oss-visualRegression': kibanaPipeline.getPostBuildWorker('visualRegression', { runbld './test/scripts/jenkins_visual_regression.sh' }), + 'oss-firefoxSmoke': kibanaPipeline.getPostBuildWorker('firefoxSmoke', { runbld('./test/scripts/jenkins_firefox_smoke.sh', 'Execute kibana-firefoxSmoke') }), + 'oss-accessibility': kibanaPipeline.getPostBuildWorker('accessibility', { runbld('./test/scripts/jenkins_accessibility.sh', 'Execute kibana-accessibility') }), + 'oss-visualRegression': kibanaPipeline.getPostBuildWorker('visualRegression', { runbld('./test/scripts/jenkins_visual_regression.sh', 'Execute kibana-visualRegression') }), ]), 'kibana-xpack-agent': kibanaPipeline.withWorkers('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ 'xpack-ciGroup1': kibanaPipeline.getXpackCiGroupWorker(1), @@ -39,9 +39,9 @@ stage("Kibana Pipeline") { // This stage is just here to help the BlueOcean UI a 'xpack-ciGroup8': kibanaPipeline.getXpackCiGroupWorker(8), 'xpack-ciGroup9': kibanaPipeline.getXpackCiGroupWorker(9), 'xpack-ciGroup10': kibanaPipeline.getXpackCiGroupWorker(10), - 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld './test/scripts/jenkins_xpack_firefox_smoke.sh' }), - 'xpack-accessibility': kibanaPipeline.getPostBuildWorker('xpack-accessibility', { runbld './test/scripts/jenkins_xpack_accessibility.sh' }), - 'xpack-visualRegression': kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld './test/scripts/jenkins_xpack_visual_regression.sh' }), + 'xpack-firefoxSmoke': kibanaPipeline.getPostBuildWorker('xpack-firefoxSmoke', { runbld('./test/scripts/jenkins_xpack_firefox_smoke.sh', 'Execute xpack-firefoxSmoke') }), + 'xpack-accessibility': kibanaPipeline.getPostBuildWorker('xpack-accessibility', { runbld('./test/scripts/jenkins_xpack_accessibility.sh', 'Execute xpack-accessibility') }), + 'xpack-visualRegression': kibanaPipeline.getPostBuildWorker('xpack-visualRegression', { runbld('./test/scripts/jenkins_xpack_visual_regression.sh', 'Execute xpack-visualRegression') }), ]), ]) } diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index f824acbc63ccda..90df352e18a001 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -68,7 +68,7 @@ def getOssCiGroupWorker(ciGroup) { "CI_GROUP=${ciGroup}", "JOB=kibana-ciGroup${ciGroup}", ]) { - runbld "./test/scripts/jenkins_ci_group.sh" + runbld("./test/scripts/jenkins_ci_group.sh", "Execute kibana-ciGroup${ciGroup}") } }) } @@ -79,7 +79,7 @@ def getXpackCiGroupWorker(ciGroup) { "CI_GROUP=${ciGroup}", "JOB=xpack-kibana-ciGroup${ciGroup}", ]) { - runbld "./test/scripts/jenkins_xpack_ci_group.sh" + runbld("./test/scripts/jenkins_xpack_ci_group.sh", "Execute xpack-kibana-ciGroup${ciGroup}") } }) } @@ -93,7 +93,7 @@ def legacyJobRunner(name) { ]) { jobRunner('linux && immutable', false) { try { - runbld('.ci/run.sh', true) + runbld('.ci/run.sh', "Execute ${name}", true) } finally { catchError { uploadAllGcsArtifacts(name) @@ -118,12 +118,15 @@ def jobRunner(label, useRamDisk, closure) { // Move to a temporary workspace, so that we can symlink the real workspace into /dev/shm def originalWorkspace = env.WORKSPACE ws('/tmp/workspace') { - sh """ - mkdir -p /dev/shm/workspace - mkdir -p '${originalWorkspace}' # create all of the directories leading up to the workspace, if they don't exist - rm --preserve-root -rf '${originalWorkspace}' # then remove just the workspace, just in case there's stuff in it - ln -s /dev/shm/workspace '${originalWorkspace}' - """ + sh( + script: """ + mkdir -p /dev/shm/workspace + mkdir -p '${originalWorkspace}' # create all of the directories leading up to the workspace, if they don't exist + rm --preserve-root -rf '${originalWorkspace}' # then remove just the workspace, just in case there's stuff in it + ln -s /dev/shm/workspace '${originalWorkspace}' + """, + label: "Move workspace to RAM - /dev/shm/workspace" + ) } } @@ -235,27 +238,33 @@ def sendKibanaMail() { } } -def bash(script) { - sh "#!/bin/bash\n${script}" +def bash(script, label) { + sh( + script: "#!/bin/bash\n${script}", + label: label + ) } def doSetup() { - runbld "./test/scripts/jenkins_setup.sh" + runbld("./test/scripts/jenkins_setup.sh", "Setup Build Environment and Dependencies") } def buildOss() { - runbld "./test/scripts/jenkins_build_kibana.sh" + runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana") } def buildXpack() { - runbld "./test/scripts/jenkins_xpack_build_kibana.sh" + runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana") } def runErrorReporter() { - bash """ - source src/dev/ci_setup/setup_env.sh - node scripts/report_failed_tests - """ + bash( + """ + source src/dev/ci_setup/setup_env.sh + node scripts/report_failed_tests + """, + "Report failed tests, if necessary" + ) } return this diff --git a/vars/runbld.groovy b/vars/runbld.groovy index 501e2421ca65b5..e52bc244c65cb8 100644 --- a/vars/runbld.groovy +++ b/vars/runbld.groovy @@ -1,11 +1,17 @@ -def call(script, enableJunitProcessing = false) { +def call(script, label, enableJunitProcessing = false) { def extraConfig = enableJunitProcessing ? "" : "--config ${env.WORKSPACE}/kibana/.ci/runbld_no_junit.yml" - sh "/usr/local/bin/runbld -d '${pwd()}' ${extraConfig} ${script}" + sh( + script: "/usr/local/bin/runbld -d '${pwd()}' ${extraConfig} ${script}", + label: label ?: script + ) } def junit() { - sh "/usr/local/bin/runbld -d '${pwd()}' ${env.WORKSPACE}/kibana/test/scripts/jenkins_runbld_junit.sh" + sh( + script: "/usr/local/bin/runbld -d '${pwd()}' ${env.WORKSPACE}/kibana/test/scripts/jenkins_runbld_junit.sh", + label: "Process JUnit reports with runbld" + ) } return this From 69431b55d59f68e89a040eaf5a8d6aa491211c48 Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Fri, 15 Nov 2019 12:18:49 -0500 Subject: [PATCH 28/28] [Canvas] Use compressed forms in sidebar (#49419) * compressed sidebar design * add back metric help, style no datasource msg * re-style remove button * re-style element status section * remove unused component import * update storyshots * clean up unused i18n values * address feedback * address i81n feedback * update storyshot * convert string to i18n * style grouped sidebar messages * update storyshots --- .../extended_template.examples.storyshot | 10 +- .../axis_config/extended_template.tsx | 8 +- .../uis/arguments/datacolumn/index.js | 2 +- .../uis/arguments/filter_group.js | 6 +- .../uis/arguments/image_upload/index.js | 1 + .../canvas_plugin_src/uis/arguments/string.js | 2 +- .../canvas_plugin_src/uis/arguments/toggle.js | 10 +- .../uis/datasources/demodata.js | 20 +--- .../uis/datasources/essql.js | 17 ++- .../uis/datasources/index.js | 4 +- .../uis/datasources/timelion.js | 68 ++++++------ .../canvas_plugin_src/uis/views/metric.js | 24 ++--- .../canvas/canvas_plugin_src/uis/views/pie.js | 48 ++++----- .../legacy/plugins/canvas/i18n/components.ts | 12 +-- .../legacy/plugins/canvas/i18n/constants.ts | 3 + .../plugins/canvas/i18n/expression_types.ts | 19 ++-- x-pack/legacy/plugins/canvas/i18n/ui.ts | 83 ++++++++------ .../arg_add_popover/arg_add_popover.scss | 4 + .../arg_add_popover/arg_add_popover.tsx | 1 + .../public/components/arg_form/arg_form.scss | 56 ++++------ .../public/components/arg_form/arg_label.js | 14 ++- .../components/arg_form/arg_simple_form.tsx | 19 ++-- .../components/datasource/datasource.scss | 25 +++++ .../datasource/datasource_component.js | 69 +++++++----- .../datasource_preview/datasource_preview.js | 8 +- .../datasource/datasource_selector.js | 13 ++- .../components/datasource/no_datasource.js | 11 +- .../components/datatable/datatable.scss | 9 +- .../element_config/element_config.js | 51 ++++++--- .../es_field_select/es_field_select.js | 1 + .../es_fields_select/es_fields_select.js | 1 + .../es_index_select/es_index_select.js | 1 + .../components/page_config/page_config.js | 26 +++-- .../shape_picker_popover.examples.storyshot | 102 ++++++++++-------- .../shape_picker_popover.tsx | 10 +- .../group_settings.examples.storyshot | 18 ++-- .../multi_element_settings.examples.storyshot | 18 ++-- .../element_settings/element_settings.tsx | 4 +- .../components/sidebar/global_config.tsx | 4 +- .../components/sidebar/group_settings.tsx | 10 +- .../sidebar/multi_element_settings.tsx | 10 +- .../public/components/sidebar/sidebar.scss | 10 +- .../components/sidebar/sidebar_section.js | 4 +- .../sidebar/sidebar_section_title.js | 2 +- .../sidebar_header/sidebar_header.scss | 6 +- .../text_style_picker/text_style_picker.js | 27 ++--- .../workpad_config/workpad_config.js | 71 ++++++------ .../extended_template.examples.storyshot | 6 +- .../simple_template.examples.storyshot | 90 ++++------------ .../series_style/extended_template.tsx | 2 +- .../series_style/simple_template.tsx | 14 +-- .../expression_types/datasources/esdocs.js | 55 +++++++--- .../plugins/canvas/public/style/main.scss | 2 +- 53 files changed, 628 insertions(+), 483 deletions(-) diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot index 5efbbe01065054..ef301c08cdfe83 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/extended_template.examples.storyshot @@ -11,7 +11,7 @@ exports[`Storyshots arguments/AxisConfig extended 1`] = ` } >
- The axis is disabled +

+ Switch on to view axis settings +

diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx index 0ec722e370b404..832f953974af4f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/extended_template.tsx @@ -71,7 +71,11 @@ export class ExtendedTemplate extends PureComponent { const isDisabled = typeof this.props.argValue === 'boolean' && this.props.argValue === false; if (isDisabled) { - return The axis is disabled; + return ( + +

{strings.getDisabledText()}

+
+ ); } const positions = { @@ -85,7 +89,7 @@ export class ExtendedTemplate extends PureComponent { return ( - + + { onChange={ev => setInputValue(ev.target.value)} /> - + - Set + {strings.getButtonSet()} setAddMode(!addMode)} flush="left"> - Cancel + {strings.getButtonCancel()} ); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js index e8c433fb8752df..a3c327da2e4dc8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/image_upload/index.js @@ -130,6 +130,7 @@ class ImageUpload extends React.Component { idSelected={urlType} onChange={this.changeUrlType} isFullWidth + className="canvasSidebar__buttonGroup" /> ); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js index d8a7188dfab286..dc31497a7da783 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/string.js @@ -26,7 +26,7 @@ const StringArgInput = ({ updateValue, value, confirm, commit, argId }) => ( /> {confirm && ( - + commit(value)}> {confirm} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js index 462537e82b164f..de19d3e29221bb 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/toggle.js @@ -19,8 +19,14 @@ const ToggleArgInput = ({ onValueChange, argValue, argId, renderError }) => { return null; } return ( - - + + ); }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js index ec492f52747c13..193d99e1c95339 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/demodata.js @@ -5,28 +5,15 @@ */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText } from '@elastic/eui'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; -import { ComponentStrings, CANVAS, DataSourceStrings } from '../../../i18n'; +import { DataSourceStrings } from '../../../i18n'; const { DemoData: strings } = DataSourceStrings; const DemodataDatasource = () => ( - -

{strings.getHeading()}

-

- {ComponentStrings.DatasourceDatasourceComponent.getChangeButtonLabel()} - ), - }} - /> -

+ +

{strings.getDescription()}

); @@ -34,7 +21,6 @@ export const demodata = () => ({ name: 'demodata', displayName: strings.getDisplayName(), help: strings.getHelp(), - // Replace this with a better icon when we have time. image: 'logoElasticStack', template: templateFromReactComponent(DemodataDatasource), }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js index 43f2fa63aff702..707f2305e1368f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/essql.js @@ -6,10 +6,10 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { EuiFormRow, EuiTextArea } from '@elastic/eui'; +import { EuiFormRow, EuiTextArea, EuiLink, EuiText } from '@elastic/eui'; import { getSimpleArg, setSimpleArg } from '../../../public/lib/arg_helpers'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; -import { DataSourceStrings } from '../../../i18n'; +import { DataSourceStrings, SQL_URL } from '../../../i18n'; const { Essql: strings } = DataSourceStrings; @@ -59,13 +59,24 @@ class EssqlDatasource extends PureComponent { const { isInvalid } = this.props; return ( - + + + {strings.getLabelAppend()} + +
+ } + >
); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js index 107d4d241d2e7c..13aa2a06306a08 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/index.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { timelion } from './timelion'; import { demodata } from './demodata'; import { essql } from './essql'; +import { timelion } from './timelion'; -export const datasourceSpecs = [timelion, demodata, essql]; +export const datasourceSpecs = [demodata, essql, timelion]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js index 06efb6a791a2de..b30e43c1c3c574 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/datasources/timelion.js @@ -12,13 +12,13 @@ import { EuiCallOut, EuiSpacer, EuiCode, - EuiText, EuiTextArea, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { getSimpleArg, setSimpleArg } from '../../../public/lib/arg_helpers'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; import { DataSourceStrings, TIMELION, CANVAS } from '../../../i18n'; +import { TooltipIcon } from '../../../public/components/tooltip_icon'; const { Timelion: strings } = DataSourceStrings; @@ -57,43 +57,12 @@ const TimelionDatasource = ({ args, updateArgs, defaultIndex }) => { return (
- -

{TIMELION}

-

{strings.getAbout()}

-
- - - - - setArg(argName, e.target.value)} - /> - - { - // TODO: Time timelion interval picker should be a drop down - } - - setArg('interval', e.target.value)} - /> - - - - - +
  • {
  • {
+ + + + } + > + setArg(argName, e.target.value)} + rows={15} + /> + + { + // TODO: Time timelion interval picker should be a drop down + } + + setArg('interval', e.target.value)} + /> +
); }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js index 213a2e0dd3b81f..33cdb5541e1726 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js @@ -16,6 +16,13 @@ export const metric = () => ({ modelArgs: [['_', { label: strings.getNumberDisplayName() }]], requiresContext: false, args: [ + { + name: 'metricFormat', + displayName: strings.getMetricFormatDisplayName(), + help: strings.getMetricFormatHelp(), + argType: 'numberFormat', + default: `"${AdvancedSettings.get('format:number:defaultPattern')}"`, + }, { name: '_', displayName: strings.getLabelDisplayName(), @@ -23,13 +30,6 @@ export const metric = () => ({ argType: 'string', default: '""', }, - { - name: 'labelFont', - displayName: strings.getLabelFontDisplayName(), - help: strings.getLabelFontHelp(), - argType: 'font', - default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, - }, { name: 'metricFont', displayName: strings.getMetricFontDisplayName(), @@ -38,11 +38,11 @@ export const metric = () => ({ default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, }, { - name: 'metricFormat', - displayName: strings.getMetricFormatDisplayName(), - help: strings.getMetricFormatHelp(), - argType: 'numberFormat', - default: `"${AdvancedSettings.get('format:number:defaultPattern')}"`, + name: 'labelFont', + displayName: strings.getLabelFontDisplayName(), + help: strings.getLabelFontHelp(), + argType: 'font', + default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, }, ], }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js index 4bb68973e80eab..783140b0c8b9ef 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js @@ -23,6 +23,16 @@ export const pie = () => ({ name: 'palette', argType: 'palette', }, + { + name: 'legend', + displayName: strings.getLegendDisplayName(), + help: strings.getLegendHelp(), + argType: 'select', + default: 'ne', + options: { + choices: legendOptions, + }, + }, { name: 'hole', displayName: strings.getHoleDisplayName(), @@ -34,13 +44,6 @@ export const pie = () => ({ max: 100, }, }, - { - name: 'labels', - displayName: strings.getLabelsDisplayName(), - help: strings.getLabelsHelp(), - argType: 'toggle', - default: true, - }, { name: 'labelRadius', displayName: strings.getLabelRadiusDisplayName(), @@ -52,16 +55,6 @@ export const pie = () => ({ max: 100, }, }, - { - name: 'legend', - displayName: strings.getLegendDisplayName(), - help: strings.getLegendHelp(), - argType: 'select', - default: 'ne', - options: { - choices: legendOptions, - }, - }, { name: 'radius', displayName: strings.getRadiusDisplayName(), @@ -69,6 +62,20 @@ export const pie = () => ({ argType: 'percentage', default: 1, }, + { + name: 'tilt', + displayName: strings.getTiltDisplayName(), + help: strings.getTiltHelp(), + argType: 'percentage', + default: 1, + }, + { + name: 'labels', + displayName: strings.getLabelsDisplayName(), + help: strings.getLabelsHelp(), + argType: 'toggle', + default: true, + }, { name: 'seriesStyle', argType: 'seriesStyle', @@ -78,13 +85,6 @@ export const pie = () => ({ name: 'font', argType: 'font', }, - { - name: 'tilt', - displayName: strings.getTiltDisplayName(), - help: strings.getTiltHelp(), - argType: 'percentage', - default: 1, - }, ], resolve({ context }) { if (getState(context) !== 'ready') { diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts index 1e6da888abf58e..5b9f6f00940f4b 100644 --- a/x-pack/legacy/plugins/canvas/i18n/components.ts +++ b/x-pack/legacy/plugins/canvas/i18n/components.ts @@ -228,11 +228,11 @@ export const ComponentStrings = { DatasourceDatasourceComponent: { getChangeButtonLabel: () => i18n.translate('xpack.canvas.datasourceDatasourceComponent.changeButtonLabel', { - defaultMessage: 'Change your data source', + defaultMessage: 'Change element data source', }), getPreviewButtonLabel: () => i18n.translate('xpack.canvas.datasourceDatasourceComponent.previewButtonLabel', { - defaultMessage: 'Preview', + defaultMessage: 'Preview data', }), getSaveButtonLabel: () => i18n.translate('xpack.canvas.datasourceDatasourceComponent.saveButtonLabel', { @@ -294,7 +294,7 @@ export const ComponentStrings = { }), getTitle: () => i18n.translate('xpack.canvas.elementConfig.title', { - defaultMessage: 'Elements', + defaultMessage: 'Element status', description: '"Elements" refers to the individual text, images, or visualizations that you can add to a Canvas workpad', }), @@ -581,7 +581,7 @@ export const ComponentStrings = { }), getBackgroundColorLabel: () => i18n.translate('xpack.canvas.pageConfig.backgroundColorLabel', { - defaultMessage: 'Background Color', + defaultMessage: 'Background', }), getNoTransitionDropDownOptionLabel: () => i18n.translate('xpack.canvas.pageConfig.transitions.noneDropDownOptionLabel', { @@ -592,7 +592,7 @@ export const ComponentStrings = { }), getTitle: () => i18n.translate('xpack.canvas.pageConfig.title', { - defaultMessage: 'Page', + defaultMessage: 'Page styles', }), getTransitionLabel: () => i18n.translate('xpack.canvas.pageConfig.transitionLabel', { @@ -1002,7 +1002,7 @@ export const ComponentStrings = { }), getTitle: () => i18n.translate('xpack.canvas.workpadConfig.title', { - defaultMessage: 'Workpad', + defaultMessage: 'Workpad settings', }), getUSLetterButtonLabel: () => i18n.translate('xpack.canvas.workpadConfig.USLetterButtonLabel', { diff --git a/x-pack/legacy/plugins/canvas/i18n/constants.ts b/x-pack/legacy/plugins/canvas/i18n/constants.ts index 3659c369ba0b6c..8aee6ca1486811 100644 --- a/x-pack/legacy/plugins/canvas/i18n/constants.ts +++ b/x-pack/legacy/plugins/canvas/i18n/constants.ts @@ -15,6 +15,7 @@ export const CSV = 'CSV'; export const DATEMATH = '`datemath`'; export const DATATABLE = '`datatable`'; export const ELASTICSEARCH = 'Elasticsearch'; +export const ELASTICSEARCH_SHORT = 'ES'; export const FONT_FAMILY = '`font-family`'; export const FONT_WEIGHT = '`font-weight`'; export const HEX = 'HEX'; @@ -32,6 +33,8 @@ export const PDF = 'PDF'; export const POST = 'POST'; export const RGB = 'RGB'; export const SQL = 'SQL'; +export const SQL_URL = + 'https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-spec.html'; export const SVG = 'SVG'; export const TIMELION = 'Timelion'; export const TINYMATH = '`TinyMath`'; diff --git a/x-pack/legacy/plugins/canvas/i18n/expression_types.ts b/x-pack/legacy/plugins/canvas/i18n/expression_types.ts index 6bc40a2758ab39..bdd190f26c97aa 100644 --- a/x-pack/legacy/plugins/canvas/i18n/expression_types.ts +++ b/x-pack/legacy/plugins/canvas/i18n/expression_types.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { LUCENE } from './constants'; +import { LUCENE, ELASTICSEARCH } from './constants'; export const ArgTypesStrings = { Color: { @@ -101,13 +101,17 @@ export const ArgTypesStrings = { i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.colorLabel', { defaultMessage: 'Color', }), + getColorValueDefault: () => + i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.colorValueDefault', { + defaultMessage: 'Auto', + }), getStyleLabel: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.styleLabel', { defaultMessage: 'Style', }), getRemoveAriaLabel: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.removeAriaLabel', { - defaultMessage: 'Remove Series Color', + defaultMessage: 'Remove series color', }), getNoSeriesTooltip: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.noSeriesTooltip', { @@ -115,11 +119,11 @@ export const ArgTypesStrings = { }), getSeriesIdentifierLabel: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.seriesIdentifierLabel', { - defaultMessage: 'Series Identifier', + defaultMessage: 'Series id', }), getSelectSeriesOption: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.selectSeriesDropDown', { - defaultMessage: 'Select Series', + defaultMessage: 'Select series', }), getLineLabel: () => i18n.translate('xpack.canvas.expressionTypes.argTypes.seriesStyle.lineLabel', { @@ -152,15 +156,18 @@ export const ExpressionDataSourceStrings = { }), getWarningTitle: () => i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.warningTitle', { - defaultMessage: 'Be careful', + defaultMessage: 'Query with caution', }), getWarning: () => i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.warningDescription', { defaultMessage: ` - The Elasticsearch Docs datasource is used to pull documents directly from Elasticsearch + This datasource pulls directly from {elasticsearch} without the use of aggregations. It is best used with low volume datasets and in situations where you need to view raw documents or plot exact, non-aggregated values on a chart.`, + values: { + elasticsearch: ELASTICSEARCH, + }, }), getIndexTitle: () => i18n.translate('xpack.canvas.expressionTypes.datasources.esdocs.indexTitle', { diff --git a/x-pack/legacy/plugins/canvas/i18n/ui.ts b/x-pack/legacy/plugins/canvas/i18n/ui.ts index b65a666aa88095..323a6c97fd967d 100644 --- a/x-pack/legacy/plugins/canvas/i18n/ui.ts +++ b/x-pack/legacy/plugins/canvas/i18n/ui.ts @@ -12,9 +12,9 @@ import { CANVAS, CSS, ELASTICSEARCH, + ELASTICSEARCH_SHORT, HEX, HTML, - KIBANA, LUCENE, MARKDOWN, MOMENTJS, @@ -35,6 +35,10 @@ export const ArgumentStrings = { i18n.translate('xpack.canvas.uis.arguments.axisConfigLabel', { defaultMessage: 'Visualization axis configuration', }), + getDisabledText: () => + i18n.translate('xpack.canvas.uis.arguments.axisConfigDisabledText', { + defaultMessage: 'Switch on to view axis settings', + }), getPositionBottom: () => i18n.translate('xpack.canvas.uis.arguments.axisConfig.position.options.bottomDropDown', { defaultMessage: 'bottom', @@ -124,6 +128,14 @@ export const ArgumentStrings = { i18n.translate('xpack.canvas.uis.arguments.filterGroup.createNewGroupLinkText', { defaultMessage: 'Create new group', }), + getButtonSet: () => + i18n.translate('xpack.canvas.uis.arguments.filterGroup.setValue', { + defaultMessage: 'Set', + }), + getButtonCancel: () => + i18n.translate('xpack.canvas.uis.arguments.filterGroup.cancelValue', { + defaultMessage: 'Cancel', + }), getDisplayName: () => i18n.translate('xpack.canvas.uis.arguments.filterGroupTitle', { defaultMessage: 'Filter Group', @@ -260,7 +272,7 @@ export const ArgumentStrings = { }), getHelp: () => i18n.translate('xpack.canvas.uis.arguments.shapeLabel', { - defaultMessage: 'Shape picker', + defaultMessage: 'Change the shape of the current element', }), }, String: { @@ -303,12 +315,20 @@ export const DataSourceStrings = { }), getHeading: () => i18n.translate('xpack.canvas.uis.dataSources.demoData.headingTitle', { - defaultMessage: 'You are using demo data', + defaultMessage: 'This element is using demo data', }), getHelp: () => i18n.translate('xpack.canvas.uis.dataSources.demoDataLabel', { defaultMessage: 'Mock data set with usernames, prices, projects, countries, and phases', }), + getDescription: () => + i18n.translate('xpack.canvas.uis.dataSources.demoDataDescription', { + defaultMessage: + 'By default, every {canvas} element is connected to the demo data source. Change the data source, above, to connect your own data.', + values: { + canvas: CANVAS, + }, + }), }, Essql: { getDisplayName: () => @@ -329,9 +349,13 @@ export const DataSourceStrings = { }), getLabel: () => i18n.translate('xpack.canvas.uis.dataSources.essql.queryTitle', { - defaultMessage: '{elasticsearch} {sql} query', + defaultMessage: 'Query', + }), + getLabelAppend: () => + i18n.translate('xpack.canvas.uis.dataSources.essql.queryTitleAppend', { + defaultMessage: 'Learn {elasticsearchShort} {sql} syntax', values: { - elasticsearch: ELASTICSEARCH, + elasticsearchShort: ELASTICSEARCH_SHORT, sql: SQL, }, }), @@ -340,10 +364,9 @@ export const DataSourceStrings = { getAbout: () => i18n.translate('xpack.canvas.uis.dataSources.timelion.aboutDetail', { defaultMessage: - '{canvas} integrates with {kibanaTimelion} application to allow you to use {timelion} queries to pull back timeseries data in a tabular format that can be used with {canvas} elements.', + 'Use {timelion} queries to pull back timeseries data that can be used with {canvas} elements.', values: { timelion: TIMELION, - kibanaTimelion: `${KIBANA}'s ${TIMELION}`, canvas: CANVAS, }, }), @@ -357,9 +380,8 @@ export const DataSourceStrings = { getIntervalHelp: () => i18n.translate('xpack.canvas.uis.dataSources.timelion.intervalLabel', { defaultMessage: - 'Accepts {elasticsearch} date math: {weeksExample}, {daysExample}, {secondsExample}, or {auto}', + 'Use date math like {weeksExample}, {daysExample}, {secondsExample}, or {auto}', values: { - elasticsearch: ELASTICSEARCH, secondsExample: '10s', daysExample: '5d', weeksExample: '1w', @@ -383,7 +405,11 @@ export const DataSourceStrings = { }), getTipsHeading: () => i18n.translate('xpack.canvas.uis.dataSources.timelion.tipsTitle', { - defaultMessage: 'Some tips', + defaultMessage: 'Tips for using {timelion} in {canvas}', + values: { + timelion: TIMELION, + canvas: CANVAS, + }, }), }, }; @@ -530,7 +556,7 @@ export const ViewStrings = { }), getValueDisplayName: () => i18n.translate('xpack.canvas.uis.views.dropdownControl.args.valueColumnTitle', { - defaultMessage: 'Values column', + defaultMessage: 'Value column', }), getValueHelp: () => i18n.translate('xpack.canvas.uis.views.dropdownControl.args.valueColumnLabel', { @@ -610,7 +636,7 @@ export const ViewStrings = { }), getNumberDisplayName: () => i18n.translate('xpack.canvas.uis.views.numberArgTitle', { - defaultMessage: 'Number', + defaultMessage: 'Value', }), getLabelDisplayName: () => i18n.translate('xpack.canvas.uis.views.metric.args.labelArgTitle', { @@ -618,7 +644,7 @@ export const ViewStrings = { }), getLabelFontDisplayName: () => i18n.translate('xpack.canvas.uis.views.metric.args.labelFontTitle', { - defaultMessage: 'Label text settings', + defaultMessage: 'Label text', }), getLabelFontHelp: () => i18n.translate('xpack.canvas.uis.views.metric.args.labelFontLabel', { @@ -626,11 +652,11 @@ export const ViewStrings = { }), getLabelHelp: () => i18n.translate('xpack.canvas.uis.views.metric.args.labelArgLabel', { - defaultMessage: 'Describes the metric', + defaultMessage: 'Enter a text label for the metric value', }), getMetricFontDisplayName: () => i18n.translate('xpack.canvas.uis.views.metric.args.metricFontTitle', { - defaultMessage: 'Metric text settings', + defaultMessage: 'Metric text', }), getMetricFontHelp: () => i18n.translate('xpack.canvas.uis.views.metric.args.metricFontLabel', { @@ -638,11 +664,11 @@ export const ViewStrings = { }), getMetricFormatDisplayName: () => i18n.translate('xpack.canvas.uis.views.metric.args.metricFormatTitle', { - defaultMessage: 'Metric Format', + defaultMessage: 'Format', }), getMetricFormatHelp: () => i18n.translate('xpack.canvas.uis.views.metric.args.metricFormatLabel', { - defaultMessage: 'Fonts, alignment and color', + defaultMessage: 'Select a format for the metric value', }), }, Pie: { @@ -676,7 +702,7 @@ export const ViewStrings = { }), getLegendDisplayName: () => i18n.translate('xpack.canvas.uis.views.pie.args.legendTitle', { - defaultMessage: 'Legend position', + defaultMessage: 'Legend', }), getLegendHelp: () => i18n.translate('xpack.canvas.uis.views.pie.args.legendLabel', { @@ -714,7 +740,7 @@ export const ViewStrings = { }), getLegendDisplayName: () => i18n.translate('xpack.canvas.uis.views.plot.args.legendTitle', { - defaultMessage: 'Legend position', + defaultMessage: 'Legend', }), getLegendHelp: () => i18n.translate('xpack.canvas.uis.views.plot.args.legendLabel', { @@ -744,7 +770,7 @@ export const ViewStrings = { }), getBarColorHelp: () => i18n.translate('xpack.canvas.uis.views.progress.args.barColorLabel', { - defaultMessage: 'Accepts HEX, RGB or HTML Color names', + defaultMessage: 'Accepts HEX, RGB or HTML color names', }), getBarWeightDisplayName: () => i18n.translate('xpack.canvas.uis.views.progress.args.barWeightTitle', { @@ -930,7 +956,7 @@ export const ViewStrings = { }), getBorderHelp: () => i18n.translate('xpack.canvas.uis.views.shape.args.borderLabel', { - defaultMessage: 'Accepts HEX, RGB or HTML Color names', + defaultMessage: 'Accepts HEX, RGB or HTML color names', }), getBorderWidthDisplayName: () => i18n.translate('xpack.canvas.uis.views.shape.args.borderWidthTitle', { @@ -950,22 +976,19 @@ export const ViewStrings = { }), getFillHelp: () => i18n.translate('xpack.canvas.uis.views.shape.args.fillLabel', { - defaultMessage: 'Accepts HEX, RGB or HTML Color names', + defaultMessage: 'Accepts HEX, RGB or HTML color names', }), getMaintainAspectDisplayName: () => i18n.translate('xpack.canvas.uis.views.shape.args.maintainAspectTitle', { - defaultMessage: 'Maintain aspect ratio', + defaultMessage: 'Fixed ratio', }), getMaintainAspectHelp: () => i18n.translate('xpack.canvas.uis.views.shape.args.maintainAspectLabel', { - defaultMessage: `Select '{true}' to maintain aspect ratio`, - values: { - true: BOOLEAN_TRUE, - }, + defaultMessage: `Enable to maintain aspect ratio`, }), getShapeDisplayName: () => i18n.translate('xpack.canvas.uis.views.shape.args.shapeTitle', { - defaultMessage: 'Select a shape', + defaultMessage: 'Select shape', }), }, Table: { @@ -988,7 +1011,7 @@ export const ViewStrings = { }), getPerPageDisplayName: () => i18n.translate('xpack.canvas.uis.views.table.args.perPageTitle', { - defaultMessage: 'Rows per page', + defaultMessage: 'Rows', }), getPerPageHelp: () => i18n.translate('xpack.canvas.uis.views.table.args.perPageLabel', { @@ -1022,7 +1045,7 @@ export const ViewStrings = { }), getFilterGroupDisplayName: () => i18n.translate('xpack.canvas.uis.views.timefilter.args.filterGroupTitle', { - defaultMessage: 'Filter group name', + defaultMessage: 'Filter group', }), getFilterGroupHelp: () => i18n.translate('xpack.canvas.uis.views.timefilter.args.filterGroupLabel', { diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.scss b/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.scss index f2cdb1444ef23a..15676d2b02490f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.scss +++ b/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.scss @@ -1,3 +1,7 @@ +.canvasArg__addArg { + margin-right: -$euiSizeS; +} + .canvasArg__addPopover { width: 250px; } diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx b/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx index e771c978d491b2..9cc6f870b9bded 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx @@ -33,6 +33,7 @@ export const ArgAddPopover = ({ options }: Props) => { iconType="plusInCircle" aria-label={strings.getAddAriaLabel()} onClick={handleClick} + className="canvasArg__addArg" /> ); diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_form.scss b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_form.scss index bef58d6bb6f5fa..3fc220d41d551a 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_form.scss +++ b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_form.scss @@ -8,6 +8,14 @@ .canvasSidebar__panel { .canvasArg--expandable:last-child { + .canvasArg__accordion { + margin-bottom: (-$euiSizeS); + } + + .canvasArg__accordion:after { + content: none; + } + .canvasArg__accordion.euiAccordion-isOpen:after { display: none; } @@ -15,12 +23,12 @@ } .canvasArg { - margin-top: $euiSize; - + margin-top: $euiSizeS; +} - .canvasArg--remove { - visibility: hidden; - } +.canvasArg__remove { + min-width: $euiSize; + padding: $euiSizeXS 0; } .canvasArg__content { @@ -29,47 +37,18 @@ .canvasArg__form { position: relative; - -} - -.canvasArg__form, -.canvasArg__accordion { - &:hover { - .canvasArg__remove { - opacity: 1; - visibility: visible; - } - } } .canvasArg__tooltip { margin-left: -$euiSizeXL; } -.canvasArg__remove { - position: absolute; - right: -$euiSizeL; - top: $euiSizeS - 2px; - border-radius: $euiBorderRadius; - border: $euiBorderThin; - background: $euiColorEmptyShade; - opacity: 0; - visibility: hidden; - transition: opacity $euiAnimSpeedNormal $euiAnimSlightResistance; - transition-delay: $euiAnimSpeedSlow; -} - .canvasArg__accordion { - padding: $euiSizeS $euiSize; - margin: 0 (-$euiSize); + padding: $euiSizeS $euiSizeM; + margin: 0 (-$euiSizeM); background: $euiColorLightestShade; position: relative; - // different spacing means leff shift - .canvasArg__remove { - right: -$euiSizeM; - } - // don't let remove button position here if this is nested in an accordion .canvasArg__form { position: static; @@ -97,3 +76,8 @@ bottom: 0; } } + +// this is a workaround since an EuiFormRow label cannot be passed in toggle.js +.canvasArg__switch { + padding-top: calc(#{$euiSizeS} * .75); +} diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_label.js b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_label.js index 143ce670d25f66..4324eed0892a59 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_label.js +++ b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_label.js @@ -6,7 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiFormRow, EuiAccordion, EuiText, EuiToolTip } from '@elastic/eui'; +import { EuiFormRow, EuiAccordion, EuiText, EuiToolTip, EuiIcon } from '@elastic/eui'; // This is what is being generated by render() from the Arg class. It is called in FunctionForm export const ArgLabel = props => { @@ -32,7 +32,17 @@ export const ArgLabel = props => { ) : ( simpleArg && ( - + + + {label} + + + } + id={argId} + > {simpleArg} ) diff --git a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_simple_form.tsx b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_simple_form.tsx index 5b45772c143730..846f912db6f84b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_simple_form.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/arg_form/arg_simple_form.tsx @@ -6,7 +6,7 @@ import React, { ReactNode, MouseEventHandler } from 'react'; import PropTypes from 'prop-types'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { TooltipIcon, IconType } from '../tooltip_icon'; import { ComponentStrings } from '../../../i18n'; @@ -41,13 +41,16 @@ export const ArgSimpleForm: React.FunctionComponent = ({ )} {!required && ( - + + + )} ); diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss index ee6c082db12178..2407dcbbce5939 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource.scss @@ -6,6 +6,31 @@ padding: 0 $euiSizeS; } +.canvasDataSource__section { + padding: $euiSizeM; +} + +.canvasDataSource__triggerButton { + @include euiTitle('xs'); + line-height: $euiSizeXXL; +} + +.canvasDataSource__triggerButtonIcon { + margin-right: $euiSizeS; +} + +.canvasDataSource__list { + padding: $euiSizeM; +} + +.canvasDataSource__card .euiCard__content { + padding-top: 0 !important; // sass-lint:disable-line no-important +} + .canvasDataSource__card + .canvasDataSource__card { margin-top: $euiSizeS; } + +.canvasDataSource__card--isCurrent { + border-color: $euiColorSecondary; +} diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js index 5f235d44791713..8b0061e047f33f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_component.js @@ -7,20 +7,23 @@ import React, { Fragment, PureComponent } from 'react'; import PropTypes from 'prop-types'; import { - EuiPanel, EuiFlexGroup, EuiFlexItem, EuiButton, - EuiButtonEmpty, EuiSpacer, + EuiIcon, + EuiCallOut, + EuiButtonEmpty, + EuiHorizontalRule, } from '@elastic/eui'; import { isEqual } from 'lodash'; -import { ComponentStrings } from '../../../i18n'; +import { ComponentStrings, DataSourceStrings } from '../../../i18n'; import { getDefaultIndex } from '../../lib/es_service'; import { DatasourceSelector } from './datasource_selector'; import { DatasourcePreview } from './datasource_preview'; const { DatasourceDatasourceComponent: strings } = ComponentStrings; +const { DemoData: demoDataStrings } = DataSourceStrings; export class DatasourceComponent extends PureComponent { static propTypes = { @@ -113,7 +116,13 @@ export class DatasourceComponent extends PureComponent { const { defaultIndex } = this.state; if (selecting) { - return ; + return ( + + ); } const datasourcePreview = previewing ? ( @@ -124,47 +133,51 @@ export class DatasourceComponent extends PureComponent { /> ) : null; + const datasourceRender = stateDatasource.render({ + args: stateArgs, + updateArgs, + datasourceDef, + isInvalid, + setInvalid, + defaultIndex, + }); + return ( - +
setSelecting(!selecting)} + className="canvasDataSource__triggerButton" + flush="left" + size="s" > - {strings.getChangeButtonLabel()} + + {stateDatasource.displayName} - {stateDatasource.render({ - args: stateArgs, - updateArgs, - datasourceDef, - isInvalid, - setInvalid, - defaultIndex, - })} - + {stateDatasource.name === 'demodata' ? ( + + {datasourceRender} + + ) : ( + datasourceRender + )} + - setPreviewing(true)} icon="check"> + setPreviewing(true)}> {strings.getPreviewButtonLabel()} - + - + {strings.getSaveButtonLabel()} - +
{datasourcePreview}
diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js index e6d2fe550a9356..13cd2c5cd11f7b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js @@ -16,6 +16,7 @@ import { EuiPanel, EuiText, EuiEmptyPrompt, + EuiSpacer, } from '@elastic/eui'; import { Datatable } from '../../datatable'; import { Error } from '../../error'; @@ -31,21 +32,22 @@ export const DatasourcePreview = ({ done, datatable }) => ( {strings.getModalTitle()} - +

{datasourceStrings.getSaveButtonLabel()}, }} />

+ {datatable.type === 'error' ? ( ) : ( - + {datatable.rows.length > 0 ? ( ) : ( diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js index 07df2a7007c4f2..92f9b92cb1f06e 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/datasource_selector.js @@ -8,17 +8,21 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiCard, EuiIcon } from '@elastic/eui'; -export const DatasourceSelector = ({ onSelect, datasources }) => ( -
+export const DatasourceSelector = ({ onSelect, datasources, current }) => ( +
{datasources.map(d => ( } - onClick={() => onSelect(d.name)} + titleElement="h5" + icon={} description={d.help} layout="horizontal" className="canvasDataSource__card" + selectable={{ + isSelected: d.name === current ? true : false, + onClick: () => onSelect(d.name), + }} /> ))}
@@ -27,4 +31,5 @@ export const DatasourceSelector = ({ onSelect, datasources }) => ( DatasourceSelector.propTypes = { onSelect: PropTypes.func.isRequired, datasources: PropTypes.array.isRequired, + current: PropTypes.string.isRequired, }; diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/no_datasource.js b/x-pack/legacy/plugins/canvas/public/components/datasource/no_datasource.js index caafa068c6b5ba..f531ee1668aef1 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/no_datasource.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/no_datasource.js @@ -6,18 +6,17 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiPanel, EuiText } from '@elastic/eui'; +import { EuiCallOut } from '@elastic/eui'; import { ComponentStrings } from '../../../i18n'; const { DatasourceNoDatasource: strings } = ComponentStrings; export const NoDatasource = () => ( - - -

{strings.getPanelTitle()}

+
+

{strings.getPanelDescription()}

- - +
+
); NoDatasource.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/datatable/datatable.scss b/x-pack/legacy/plugins/canvas/public/components/datatable/datatable.scss index daccfdff5d34b7..bd11bff18e0917 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datatable/datatable.scss +++ b/x-pack/legacy/plugins/canvas/public/components/datatable/datatable.scss @@ -4,6 +4,7 @@ display: flex; flex-direction: column; justify-content: space-between; + font-size: $euiFontSizeS; .canvasDataTable__tableWrapper { @include euiScrollBar; @@ -12,7 +13,8 @@ overflow: auto; // removes white square in the scrollbar corner - &::-webkit-scrollbar-corner { // sass-lint:disable-line no-vendor-prefixes + // sass-lint:disable no-vendor-prefixes + &::-webkit-scrollbar-corner { background: transparent; } } @@ -21,6 +23,8 @@ width: 100%; display: flex; justify-content: space-around; + padding: $euiSizeS; + border-top: $euiBorderThin; } .canvasDataTable__table { @@ -30,7 +34,8 @@ .canvasDataTable__th, .canvasDataTable__td { text-align: left; - padding: $euiSizeS; + padding: $euiSizeS $euiSizeXS; + border-bottom: $euiBorderThin; } .canvasDataTable__th { diff --git a/x-pack/legacy/plugins/canvas/public/components/element_config/element_config.js b/x-pack/legacy/plugins/canvas/public/components/element_config/element_config.js index 76007994e56bf3..5d710ef8835482 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_config/element_config.js +++ b/x-pack/legacy/plugins/canvas/public/components/element_config/element_config.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiStat, EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiAccordion, EuiText, EuiSpacer } from '@elastic/eui'; import PropTypes from 'prop-types'; -import React, { Fragment } from 'react'; +import React from 'react'; import { ComponentStrings } from '../../../i18n'; const { ElementConfig: strings } = ComponentStrings; @@ -20,26 +20,51 @@ export const ElementConfig = ({ elementStats }) => { const progress = total > 0 ? Math.round(((ready + error) / total) * 100) : 100; return ( - - -

{strings.getTitle()}

-
- - + + {strings.getTitle()} +
+ } + initialIsOpen={false} + > + + - + - + - + - + - + ); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/es_field_select/es_field_select.js b/x-pack/legacy/plugins/canvas/public/components/es_field_select/es_field_select.js index 636d9b0006ac61..11c8ab88a4cba6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/es_field_select/es_field_select.js +++ b/x-pack/legacy/plugins/canvas/public/components/es_field_select/es_field_select.js @@ -28,6 +28,7 @@ export const ESFieldSelect = ({ value, fields = [], onChange, onFocus, onBlur }) onBlur={onBlur} singleSelection={{ asPlainText: true }} isClearable={false} + compressed /> ); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/es_fields_select/es_fields_select.js b/x-pack/legacy/plugins/canvas/public/components/es_fields_select/es_fields_select.js index fedb4aba7d3d09..ca2cac5a647931 100644 --- a/x-pack/legacy/plugins/canvas/public/components/es_fields_select/es_fields_select.js +++ b/x-pack/legacy/plugins/canvas/public/components/es_fields_select/es_fields_select.js @@ -25,6 +25,7 @@ export const ESFieldsSelect = ({ selected, fields, onChange, onFocus, onBlur }) className="canvasFieldsSelect" onFocus={onFocus} onBlur={onBlur} + compressed /> ); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/es_index_select/es_index_select.js b/x-pack/legacy/plugins/canvas/public/components/es_index_select/es_index_select.js index edc4506f20bda4..8f1a4932a5e6ce 100644 --- a/x-pack/legacy/plugins/canvas/public/components/es_index_select/es_index_select.js +++ b/x-pack/legacy/plugins/canvas/public/components/es_index_select/es_index_select.js @@ -32,6 +32,7 @@ export const ESIndexSelect = ({ value, loading, indices, onChange, onFocus, onBl singleSelection={{ asPlainText: true }} isClearable={false} onCreateOption={input => onChange(input || defaultIndex)} + compressed /> ); }; diff --git a/x-pack/legacy/plugins/canvas/public/components/page_config/page_config.js b/x-pack/legacy/plugins/canvas/public/components/page_config/page_config.js index 2586b4ec61f04e..583bf1427aab1f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/page_config/page_config.js +++ b/x-pack/legacy/plugins/canvas/public/components/page_config/page_config.js @@ -6,7 +6,15 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import { EuiCard, EuiFormRow, EuiTitle, EuiSpacer, EuiSelect } from '@elastic/eui'; +import { + EuiCard, + EuiFormRow, + EuiTitle, + EuiSpacer, + EuiSelect, + EuiToolTip, + EuiIcon, +} from '@elastic/eui'; import { WorkpadColorPicker } from '../workpad_color_picker'; import { ComponentStrings } from '../../../i18n'; @@ -22,14 +30,20 @@ export const PageConfig = ({ }) => { return ( - +

{strings.getTitle()}

- + + + {strings.getBackgroundColorLabel()}{' '} + + + + } > diff --git a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.examples.storyshot index 426c07dac497ec..a23452a43ae1e4 100644 --- a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/__examples__/__snapshots__/shape_picker_popover.examples.storyshot @@ -13,20 +13,24 @@ exports[`Storyshots components/Shapes/ShapePickerPopover default 1`] = `
- + +
`; @@ -44,27 +48,31 @@ exports[`Storyshots components/Shapes/ShapePickerPopover interactive 1`] = `
- + /> + +
`; @@ -82,27 +90,31 @@ exports[`Storyshots components/Shapes/ShapePickerPopover shape selected 1`] = `
- + /> + +
`; diff --git a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx index 472a14071208a0..970f72da698ba0 100644 --- a/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/shape_picker_popover/shape_picker_popover.tsx @@ -6,7 +6,7 @@ import React, { MouseEvent } from 'react'; import PropTypes from 'prop-types'; -import { EuiLink } from '@elastic/eui'; +import { EuiLink, EuiPanel } from '@elastic/eui'; import { Popover } from '../popover'; import { ShapePicker } from '../shape_picker'; import { ShapePreview } from '../shape_preview'; @@ -21,9 +21,11 @@ interface Props { export const ShapePickerPopover = ({ shapes, onChange, value }: Props) => { const button = (handleClick: (ev: MouseEvent) => void) => ( - - - + + + + + ); return ( diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.examples.storyshot index dc80af01f121f6..1655320700f87f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/group_settings.examples.storyshot @@ -2,13 +2,17 @@ exports[`Storyshots components/Sidebar/GroupSettings default 1`] = `
-

- Ungroup (U) to edit individual element settings. -

-

- Save this group as a new element to re-use it throughout your workpad. -

+
+

+ Ungroup (U) to edit individual element settings. +

+

+ Save this group as a new element to re-use it throughout your workpad. +

+
`; diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.examples.storyshot index b9b13ae36e7308..49e804640081d2 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/__examples__/__snapshots__/multi_element_settings.examples.storyshot @@ -2,13 +2,17 @@ exports[`Storyshots components/Sidebar/MultiElementSettings default 1`] = `
-

- Multiple elements are currently selected. -

-

- Deselect these elements to edit their individual settings, press (G) to group them, or save this selection as a new element to re-use it throughout your workpad. -

+
+

+ Multiple elements are currently selected. +

+

+ Deselect these elements to edit their individual settings, press (G) to group them, or save this selection as a new element to re-use it throughout your workpad. +

+
`; diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx index 6d884c05cd13ac..74f4887601d30c 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx @@ -6,7 +6,7 @@ import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; -import { EuiSpacer, EuiTabbedContent } from '@elastic/eui'; +import { EuiTabbedContent } from '@elastic/eui'; // @ts-ignore unconverted component import { Datasource } from '../../datasource'; // @ts-ignore unconverted component @@ -30,7 +30,6 @@ export const ElementSettings: FunctionComponent = ({ element }) => { name: strings.getDisplayTabLabel(), content: (
-
@@ -42,7 +41,6 @@ export const ElementSettings: FunctionComponent = ({ element }) => { name: strings.getDataTabLabel(), content: (
-
), diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/global_config.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar/global_config.tsx index a5920ee1974607..2e241681ccc6a7 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/global_config.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/global_config.tsx @@ -20,10 +20,10 @@ export const GlobalConfig: FunctionComponent = () => ( - + - + ); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/group_settings.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar/group_settings.tsx index b46465d9ec7755..95d9035774a6ae 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/group_settings.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/group_settings.tsx @@ -11,8 +11,10 @@ import { ComponentStrings } from '../../../i18n'; const { GroupSettings: strings } = ComponentStrings; export const GroupSettings: FunctionComponent = () => ( - -

{strings.getUngroupDescription()}

-

{strings.getSaveGroupDescription()}

-
+
+ +

{strings.getUngroupDescription()}

+

{strings.getSaveGroupDescription()}

+
+
); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/multi_element_settings.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar/multi_element_settings.tsx index 2de3a805c95e9b..999c1c2daaf5b5 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/multi_element_settings.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/multi_element_settings.tsx @@ -11,8 +11,10 @@ import { ComponentStrings } from '../../../i18n'; const { MultiElementSettings: strings } = ComponentStrings; export const MultiElementSettings: FunctionComponent = () => ( - -

{strings.getMultipleElementsDescription()}

-

{strings.getMultipleElementsActionsDescription()}

-
+
+ +

{strings.getMultipleElementsDescription()}

+

{strings.getMultipleElementsActionsDescription()}

+
+
); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar.scss b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar.scss index f9ce6f3cfb555d..338d515165e43c 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar.scss +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar.scss @@ -2,7 +2,6 @@ @include euiScrollBar; width: 100%; - padding: $euiSizeM; max-height: 100vh; overflow-y: auto; overflow-x: hidden; @@ -25,6 +24,15 @@ margin-bottom: $euiSizeS; } +.canvasSidebar__panel { + border-bottom: $euiBorderThin; + padding: $euiSizeS $euiSizeM; + + &--isEmpty { + border-bottom: none; + } +} + .canvasSidebar__panel-noMinWidth .euiButton { min-width: 0; } diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section.js b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section.js index 29ca72a9737a1f..bf149a6c2acb89 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section.js +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section.js @@ -7,10 +7,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiPanel } from '@elastic/eui'; - export const SidebarSection = ({ children }) => ( - {children} +
{children}
); SidebarSection.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section_title.js b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section_title.js index 192786ae86a450..8e1522eae8dcc3 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section_title.js +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_section_title.js @@ -10,7 +10,7 @@ import { EuiTitle, EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; export const SidebarSectionTitle = ({ title, tip, children }) => { const formattedTitle = ( - +

{title}

); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.scss b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.scss index 24453fcf0411ed..92b0c50a6be4f5 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.scss +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.scss @@ -1,3 +1,7 @@ .canvasLayout__sidebarHeader { - padding: ($euiSizeXS * .5) 0; + padding: calc(#{$euiSizeM} + 2px) $euiSizeS; } + +.canvasLayout__sidebarHeaderWorkpad { + padding: calc(#{$euiSizeS} * .75) 0; +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js b/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js index 9693540769d50a..1a441814750912 100644 --- a/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js +++ b/x-pack/legacy/plugins/canvas/public/components/text_style_picker/text_style_picker.js @@ -105,22 +105,30 @@ export const TextStylePicker = ({ return (
+ + doChange('family', value)} /> + doChange('size', Number(e.target.value))} options={fontSizes.map(size => ({ text: String(size), value: size }))} + prepend="Size" /> - - doChange('family', value)} /> - - + - + + + doChange('color', value)} + colors={colors} + /> + @@ -138,13 +147,7 @@ export const TextStylePicker = ({ isIconOnly idSelected={align} onChange={onAlignmentChange} - /> - - - doChange('color', value)} - colors={colors} + className="canvasSidebar__buttonGroup" /> diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_config/workpad_config.js b/x-pack/legacy/plugins/canvas/public/components/workpad_config/workpad_config.js index ec7386bddace6a..7dfc378432b573 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_config/workpad_config.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_config/workpad_config.js @@ -67,9 +67,11 @@ export class WorkpadConfig extends PureComponent { return (
- -

{strings.getTitle()}

-
+
+ +

{strings.getTitle()}

+
+
@@ -129,37 +131,38 @@ export class WorkpadConfig extends PureComponent {
- - - - {strings.getGlobalCSSLabel()} - - - } - > -
- this.setState({ css: e.target.value })} - rows={10} - /> - - setWorkpadCSS(css || DEFAULT_WORKPAD_CSS)}> - {strings.getApplyStylesheetButtonLabel()} - - -
-
+
+ + + {strings.getGlobalCSSLabel()} + + + } + > +
+ this.setState({ css: e.target.value })} + rows={10} + /> + + setWorkpadCSS(css || DEFAULT_WORKPAD_CSS)}> + {strings.getApplyStylesheetButtonLabel()} + + +
+
+
); } diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot index 5525df639be016..ffe87129c76fae 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot @@ -12,7 +12,7 @@ exports[`Storyshots arguments/SeriesStyle extended 1`] = ` >
- Series Identifier + Series id
- Select Series + Select series