From ce693b01963d2178dd47279b0de689f8bdae222c Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 24 Jun 2020 09:01:13 -0500 Subject: [PATCH 01/82] [APM] Storybook theme fixes (#69730) (#69794) * [APM] Storybook theme fixes The changes adding theme support in #69362 broke some of the Storybook stories. Add decorators to wrap some of the stories in the theme context. This should be done in a global decorator, but our current storybook setup doesn't support this. It also would be nice to be able to switch between light/dark mode, but that's something we can add in the future. * Remove unused import * Adds missing decorator to cytoscape examples + adds a new real-world example Co-authored-by: Oliver Gupte Co-authored-by: Elastic Machine Co-authored-by: Oliver Gupte Co-authored-by: Elastic Machine --- .../__stories__/Cytoscape.stories.tsx | 107 +- .../CytoscapeExampleData.stories.tsx | 434 ++-- .../example_response_one_domain_many_ips.json | 2122 +++++++++++++++++ .../index.stories.tsx | 38 +- 4 files changed, 2434 insertions(+), 267 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/example_response_one_domain_many_ips.json diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx index 28cb7a6f9d291..aee392b53298a 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx @@ -10,60 +10,63 @@ import cytoscape from 'cytoscape'; import React from 'react'; import { Cytoscape } from '../Cytoscape'; import { iconForNode } from '../icons'; +import { EuiThemeProvider } from '../../../../../../observability/public'; -storiesOf('app/ServiceMap/Cytoscape', module).add( - 'example', - () => { - const elements: cytoscape.ElementDefinition[] = [ - { - data: { - id: 'opbeans-python', - 'service.name': 'opbeans-python', - 'agent.name': 'python', - }, - }, - { - data: { - id: 'opbeans-node', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - }, - { - data: { - id: 'opbeans-ruby', - 'service.name': 'opbeans-ruby', - 'agent.name': 'ruby', - }, - }, - { data: { source: 'opbeans-python', target: 'opbeans-node' } }, - { - data: { - bidirectional: true, - source: 'opbeans-python', - target: 'opbeans-ruby', - }, - }, - ]; - const height = 300; - const width = 1340; - const serviceName = 'opbeans-python'; - return ( - - ); - }, - { - info: { - propTables: false, - source: false, +storiesOf('app/ServiceMap/Cytoscape', module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'example', + () => { + const elements: cytoscape.ElementDefinition[] = [ + { + data: { + id: 'opbeans-python', + 'service.name': 'opbeans-python', + 'agent.name': 'python', + }, + }, + { + data: { + id: 'opbeans-node', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + }, + { + data: { + id: 'opbeans-ruby', + 'service.name': 'opbeans-ruby', + 'agent.name': 'ruby', + }, + }, + { data: { source: 'opbeans-python', target: 'opbeans-node' } }, + { + data: { + bidirectional: true, + source: 'opbeans-python', + target: 'opbeans-ruby', + }, + }, + ]; + const height = 300; + const width = 1340; + const serviceName = 'opbeans-python'; + return ( + + ); }, - } -); + { + info: { + propTables: false, + source: false, + }, + } + ); storiesOf('app/ServiceMap/Cytoscape', module).add( 'node icons', diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/CytoscapeExampleData.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/CytoscapeExampleData.stories.tsx index 3aced1b33dcac..44278b2846128 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/CytoscapeExampleData.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/CytoscapeExampleData.stories.tsx @@ -6,23 +6,25 @@ /* eslint-disable no-console */ import { + EuiButton, + EuiCodeEditor, + EuiFieldNumber, + EuiFilePicker, EuiFlexGroup, EuiFlexItem, - EuiButton, EuiForm, - EuiFieldNumber, - EuiToolTip, - EuiCodeEditor, EuiSpacer, - EuiFilePicker, + EuiToolTip, } from '@elastic/eui'; import { storiesOf } from '@storybook/react'; -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; +import { EuiThemeProvider } from '../../../../../../observability/public'; import { Cytoscape } from '../Cytoscape'; -import { generateServiceMapElements } from './generate_service_map_elements'; -import exampleResponseOpbeansBeats from './example_response_opbeans_beats.json'; import exampleResponseHipsterStore from './example_response_hipster_store.json'; +import exampleResponseOpbeansBeats from './example_response_opbeans_beats.json'; import exampleResponseTodo from './example_response_todo.json'; +import exampleResponseOneDomainManyIPs from './example_response_one_domain_many_ips.json'; +import { generateServiceMapElements } from './generate_service_map_elements'; const STORYBOOK_PATH = 'app/ServiceMap/Cytoscape/Example data'; @@ -34,151 +36,155 @@ function setSessionJson(json: string) { window.sessionStorage.setItem(SESSION_STORAGE_KEY, json); } -storiesOf(STORYBOOK_PATH, module).add( - 'Generate map', - () => { - const [size, setSize] = useState(10); - const [json, setJson] = useState(''); - const [elements, setElements] = useState( - generateServiceMapElements(size) - ); - - return ( -
- - - { - setElements(generateServiceMapElements(size)); - setJson(''); - }} - > - Generate service map - - - - - setSize(e.target.valueAsNumber)} - /> - - - - { - setJson(JSON.stringify({ elements }, null, 2)); - }} - > - Get JSON - - - - - - - {json && ( - - )} -
- ); - }, - { - info: { propTables: false, source: false }, - } -); - -storiesOf(STORYBOOK_PATH, module).add( - 'Map from JSON', - () => { - const [json, setJson] = useState( - getSessionJson() || JSON.stringify(exampleResponseTodo, null, 2) - ); - const [error, setError] = useState(); - - const [elements, setElements] = useState([]); - useEffect(() => { - try { - setElements(JSON.parse(json).elements); - } catch (e) { - setError(e.message); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( -
- - +storiesOf(STORYBOOK_PATH, module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'Generate map', + () => { + const [size, setSize] = useState(10); + const [json, setJson] = useState(''); + const [elements, setElements] = useState( + generateServiceMapElements(size) + ); + + return ( +
- { - setJson(value); + { + setElements(generateServiceMapElements(size)); + setJson(''); }} - /> + > + Generate service map + - - { - const item = event?.item(0); - - if (item) { - const f = new FileReader(); - f.onload = (onloadEvent) => { - const result = onloadEvent?.target?.result; - if (typeof result === 'string') { - setJson(result); - } - }; - f.readAsText(item); - } - }} + + setSize(e.target.valueAsNumber)} /> - - { - try { - setElements(JSON.parse(json).elements); - setSessionJson(json); - setError(undefined); - } catch (e) { - setError(e.message); - } - }} - > - Render JSON - - + + + + { + setJson(JSON.stringify({ elements }, null, 2)); + }} + > + Get JSON + - -
- ); - }, - { - info: { - propTables: false, - source: false, - text: ` + + + + {json && ( + + )} +
+ ); + }, + { + info: { propTables: false, source: false }, + } + ); + +storiesOf(STORYBOOK_PATH, module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'Map from JSON', + () => { + const [json, setJson] = useState( + getSessionJson() || JSON.stringify(exampleResponseTodo, null, 2) + ); + const [error, setError] = useState(); + + const [elements, setElements] = useState([]); + useEffect(() => { + try { + setElements(JSON.parse(json).elements); + } catch (e) { + setError(e.message); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+ + + + + { + setJson(value); + }} + /> + + + + { + const item = event?.item(0); + + if (item) { + const f = new FileReader(); + f.onload = (onloadEvent) => { + const result = onloadEvent?.target?.result; + if (typeof result === 'string') { + setJson(result); + } + }; + f.readAsText(item); + } + }} + /> + + { + try { + setElements(JSON.parse(json).elements); + setSessionJson(json); + setError(undefined); + } catch (e) { + setError(e.message); + } + }} + > + Render JSON + + + + + +
+ ); + }, + { + info: { + propTables: false, + source: false, + text: ` Enter JSON map data into the text box or upload a file and click "Render JSON" to see the results. You can enable a download button on the service map by putting \`\`\` @@ -186,60 +192,86 @@ storiesOf(STORYBOOK_PATH, module).add( \`\`\` into the JavaScript console and reloading the page.`, + }, + } + ); + +storiesOf(STORYBOOK_PATH, module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'Todo app', + () => { + return ( +
+ +
+ ); + }, + { + info: { propTables: false, source: false }, + } + ); + +storiesOf(STORYBOOK_PATH, module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'Opbeans + beats', + () => { + return ( +
+ +
+ ); + }, + { + info: { propTables: false, source: false }, + } + ); + +storiesOf(STORYBOOK_PATH, module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'Hipster store', + () => { + return ( +
+ +
+ ); + }, + { + info: { propTables: false, source: false }, + } + ); + +storiesOf(STORYBOOK_PATH, module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'Node resolves one domain name to many IPs', + () => { + return ( +
+ +
+ ); }, - } -); - -storiesOf(STORYBOOK_PATH, module).add( - 'Todo app', - () => { - return ( -
- -
- ); - }, - { - info: { propTables: false, source: false }, - } -); - -storiesOf(STORYBOOK_PATH, module).add( - 'Opbeans + beats', - () => { - return ( -
- -
- ); - }, - { - info: { propTables: false, source: false }, - } -); - -storiesOf(STORYBOOK_PATH, module).add( - 'Hipster store', - () => { - return ( -
- -
- ); - }, - { - info: { propTables: false, source: false }, - } -); + { + info: { propTables: false, source: false }, + } + ); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/example_response_one_domain_many_ips.json b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/example_response_one_domain_many_ips.json new file mode 100644 index 0000000000000..f9b8a273d8577 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/example_response_one_domain_many_ips.json @@ -0,0 +1,2122 @@ +{ + "elements": [ + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.99:80", + "id": "artifact_api~>192.0.2.99:80", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "http", + "span.destination.service.resource": "192.0.2.99:80", + "span.type": "external", + "id": ">192.0.2.99:80", + "label": ">192.0.2.99:80" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.47:443", + "id": "artifact_api~>192.0.2.47:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.47:443", + "span.type": "external", + "id": ">192.0.2.47:443", + "label": ">192.0.2.47:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.13:443", + "id": "artifact_api~>192.0.2.13:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.13:443", + "span.type": "external", + "id": ">192.0.2.13:443", + "label": ">192.0.2.13:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.106:443", + "id": "artifact_api~>192.0.2.106:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.106:443", + "span.type": "external", + "id": ">192.0.2.106:443", + "label": ">192.0.2.106:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.83:443", + "id": "artifact_api~>192.0.2.83:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.83:443", + "span.type": "external", + "id": ">192.0.2.83:443", + "label": ">192.0.2.83:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.111:443", + "id": "artifact_api~>192.0.2.111:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.111:443", + "span.type": "external", + "id": ">192.0.2.111:443", + "label": ">192.0.2.111:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.189:443", + "id": "artifact_api~>192.0.2.189:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.189:443", + "span.type": "external", + "id": ">192.0.2.189:443", + "label": ">192.0.2.189:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.148:443", + "id": "artifact_api~>192.0.2.148:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.148:443", + "span.type": "external", + "id": ">192.0.2.148:443", + "label": ">192.0.2.148:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.39:443", + "id": "artifact_api~>192.0.2.39:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.39:443", + "span.type": "external", + "id": ">192.0.2.39:443", + "label": ">192.0.2.39:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.42:443", + "id": "artifact_api~>192.0.2.42:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.42:443", + "span.type": "external", + "id": ">192.0.2.42:443", + "label": ">192.0.2.42:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.240:443", + "id": "artifact_api~>192.0.2.240:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.240:443", + "span.type": "external", + "id": ">192.0.2.240:443", + "label": ">192.0.2.240:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.156:443", + "id": "artifact_api~>192.0.2.156:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.156:443", + "span.type": "external", + "id": ">192.0.2.156:443", + "label": ">192.0.2.156:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.245:443", + "id": "artifact_api~>192.0.2.245:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.245:443", + "span.type": "external", + "id": ">192.0.2.245:443", + "label": ">192.0.2.245:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.198:443", + "id": "artifact_api~>192.0.2.198:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.198:443", + "span.type": "external", + "id": ">192.0.2.198:443", + "label": ">192.0.2.198:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.77:443", + "id": "artifact_api~>192.0.2.77:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.77:443", + "span.type": "external", + "id": ">192.0.2.77:443", + "label": ">192.0.2.77:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.8:443", + "id": "artifact_api~>192.0.2.8:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.8:443", + "span.type": "external", + "id": ">192.0.2.8:443", + "label": ">192.0.2.8:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.69:443", + "id": "artifact_api~>192.0.2.69:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.69:443", + "span.type": "external", + "id": ">192.0.2.69:443", + "label": ">192.0.2.69:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.5:443", + "id": "artifact_api~>192.0.2.5:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.5:443", + "span.type": "external", + "id": ">192.0.2.5:443", + "label": ">192.0.2.5:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.139:443", + "id": "artifact_api~>192.0.2.139:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.139:443", + "span.type": "external", + "id": ">192.0.2.139:443", + "label": ">192.0.2.139:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.113:443", + "id": "artifact_api~>192.0.2.113:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.113:443", + "span.type": "external", + "id": ">192.0.2.113:443", + "label": ">192.0.2.113:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.2:443", + "id": "artifact_api~>192.0.2.2:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.2:443", + "span.type": "external", + "id": ">192.0.2.2:443", + "label": ">192.0.2.2:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.213:443", + "id": "artifact_api~>192.0.2.213:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.213:443", + "span.type": "external", + "id": ">192.0.2.213:443", + "label": ">192.0.2.213:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.153:443", + "id": "artifact_api~>192.0.2.153:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.153:443", + "span.type": "external", + "id": ">192.0.2.153:443", + "label": ">192.0.2.153:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.36:443", + "id": "artifact_api~>192.0.2.36:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.36:443", + "span.type": "external", + "id": ">192.0.2.36:443", + "label": ">192.0.2.36:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.164:443", + "id": "artifact_api~>192.0.2.164:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.164:443", + "span.type": "external", + "id": ">192.0.2.164:443", + "label": ">192.0.2.164:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.190:443", + "id": "artifact_api~>192.0.2.190:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.190:443", + "span.type": "external", + "id": ">192.0.2.190:443", + "label": ">192.0.2.190:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.9:443", + "id": "artifact_api~>192.0.2.9:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.9:443", + "span.type": "external", + "id": ">192.0.2.9:443", + "label": ">192.0.2.9:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.210:443", + "id": "artifact_api~>192.0.2.210:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.210:443", + "span.type": "external", + "id": ">192.0.2.210:443", + "label": ">192.0.2.210:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.21:443", + "id": "artifact_api~>192.0.2.21:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.21:443", + "span.type": "external", + "id": ">192.0.2.21:443", + "label": ">192.0.2.21:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.176:443", + "id": "artifact_api~>192.0.2.176:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.176:443", + "span.type": "external", + "id": ">192.0.2.176:443", + "label": ">192.0.2.176:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.81:443", + "id": "artifact_api~>192.0.2.81:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.81:443", + "span.type": "external", + "id": ">192.0.2.81:443", + "label": ">192.0.2.81:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.118:443", + "id": "artifact_api~>192.0.2.118:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.118:443", + "span.type": "external", + "id": ">192.0.2.118:443", + "label": ">192.0.2.118:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.103:443", + "id": "artifact_api~>192.0.2.103:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.103:443", + "span.type": "external", + "id": ">192.0.2.103:443", + "label": ">192.0.2.103:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.3:443", + "id": "artifact_api~>192.0.2.3:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.3:443", + "span.type": "external", + "id": ">192.0.2.3:443", + "label": ">192.0.2.3:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.135:443", + "id": "artifact_api~>192.0.2.135:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.135:443", + "span.type": "external", + "id": ">192.0.2.135:443", + "label": ">192.0.2.135:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.26:443", + "id": "artifact_api~>192.0.2.26:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.26:443", + "span.type": "external", + "id": ">192.0.2.26:443", + "label": ">192.0.2.26:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.185:443", + "id": "artifact_api~>192.0.2.185:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.185:443", + "span.type": "external", + "id": ">192.0.2.185:443", + "label": ">192.0.2.185:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.173:443", + "id": "artifact_api~>192.0.2.173:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.173:443", + "span.type": "external", + "id": ">192.0.2.173:443", + "label": ">192.0.2.173:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.45:443", + "id": "artifact_api~>192.0.2.45:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.45:443", + "span.type": "external", + "id": ">192.0.2.45:443", + "label": ">192.0.2.45:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.144:443", + "id": "artifact_api~>192.0.2.144:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.144:443", + "span.type": "external", + "id": ">192.0.2.144:443", + "label": ">192.0.2.144:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.165:443", + "id": "artifact_api~>192.0.2.165:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.165:443", + "span.type": "external", + "id": ">192.0.2.165:443", + "label": ">192.0.2.165:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.119:443", + "id": "artifact_api~>192.0.2.119:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.119:443", + "span.type": "external", + "id": ">192.0.2.119:443", + "label": ">192.0.2.119:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.186:443", + "id": "artifact_api~>192.0.2.186:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.186:443", + "span.type": "external", + "id": ">192.0.2.186:443", + "label": ">192.0.2.186:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.54:443", + "id": "artifact_api~>192.0.2.54:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.54:443", + "span.type": "external", + "id": ">192.0.2.54:443", + "label": ">192.0.2.54:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.23:443", + "id": "artifact_api~>192.0.2.23:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.23:443", + "span.type": "external", + "id": ">192.0.2.23:443", + "label": ">192.0.2.23:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.34:443", + "id": "artifact_api~>192.0.2.34:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.34:443", + "span.type": "external", + "id": ">192.0.2.34:443", + "label": ">192.0.2.34:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.169:443", + "id": "artifact_api~>192.0.2.169:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.169:443", + "span.type": "external", + "id": ">192.0.2.169:443", + "label": ">192.0.2.169:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.226:443", + "id": "artifact_api~>192.0.2.226:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.226:443", + "span.type": "external", + "id": ">192.0.2.226:443", + "label": ">192.0.2.226:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.82:443", + "id": "artifact_api~>192.0.2.82:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.82:443", + "span.type": "external", + "id": ">192.0.2.82:443", + "label": ">192.0.2.82:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.132:443", + "id": "artifact_api~>192.0.2.132:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.132:443", + "span.type": "external", + "id": ">192.0.2.132:443", + "label": ">192.0.2.132:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.78:443", + "id": "artifact_api~>192.0.2.78:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.78:443", + "span.type": "external", + "id": ">192.0.2.78:443", + "label": ">192.0.2.78:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.71:443", + "id": "artifact_api~>192.0.2.71:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.71:443", + "span.type": "external", + "id": ">192.0.2.71:443", + "label": ">192.0.2.71:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.48:443", + "id": "artifact_api~>192.0.2.48:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.48:443", + "span.type": "external", + "id": ">192.0.2.48:443", + "label": ">192.0.2.48:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.107:443", + "id": "artifact_api~>192.0.2.107:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.107:443", + "span.type": "external", + "id": ">192.0.2.107:443", + "label": ">192.0.2.107:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.239:443", + "id": "artifact_api~>192.0.2.239:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.239:443", + "span.type": "external", + "id": ">192.0.2.239:443", + "label": ">192.0.2.239:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.209:443", + "id": "artifact_api~>192.0.2.209:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.209:443", + "span.type": "external", + "id": ">192.0.2.209:443", + "label": ">192.0.2.209:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.248:443", + "id": "artifact_api~>192.0.2.248:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.248:443", + "span.type": "external", + "id": ">192.0.2.248:443", + "label": ">192.0.2.248:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.18:443", + "id": "artifact_api~>192.0.2.18:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.18:443", + "span.type": "external", + "id": ">192.0.2.18:443", + "label": ">192.0.2.18:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.228:443", + "id": "artifact_api~>192.0.2.228:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.228:443", + "span.type": "external", + "id": ">192.0.2.228:443", + "label": ">192.0.2.228:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.145:443", + "id": "artifact_api~>192.0.2.145:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.145:443", + "span.type": "external", + "id": ">192.0.2.145:443", + "label": ">192.0.2.145:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.25:443", + "id": "artifact_api~>192.0.2.25:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.25:443", + "span.type": "external", + "id": ">192.0.2.25:443", + "label": ">192.0.2.25:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.162:443", + "id": "artifact_api~>192.0.2.162:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.162:443", + "span.type": "external", + "id": ">192.0.2.162:443", + "label": ">192.0.2.162:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.202:443", + "id": "artifact_api~>192.0.2.202:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.202:443", + "span.type": "external", + "id": ">192.0.2.202:443", + "label": ">192.0.2.202:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.60:443", + "id": "artifact_api~>192.0.2.60:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.60:443", + "span.type": "external", + "id": ">192.0.2.60:443", + "label": ">192.0.2.60:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.59:443", + "id": "artifact_api~>192.0.2.59:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.59:443", + "span.type": "external", + "id": ">192.0.2.59:443", + "label": ">192.0.2.59:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.114:443", + "id": "artifact_api~>192.0.2.114:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.114:443", + "span.type": "external", + "id": ">192.0.2.114:443", + "label": ">192.0.2.114:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.215:443", + "id": "artifact_api~>192.0.2.215:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.215:443", + "span.type": "external", + "id": ">192.0.2.215:443", + "label": ">192.0.2.215:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.238:443", + "id": "artifact_api~>192.0.2.238:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.238:443", + "span.type": "external", + "id": ">192.0.2.238:443", + "label": ">192.0.2.238:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.160:443", + "id": "artifact_api~>192.0.2.160:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.160:443", + "span.type": "external", + "id": ">192.0.2.160:443", + "label": ">192.0.2.160:443" + } + } + }, + { + "data": { + "source": "artifact_api", + "target": ">192.0.2.70:443", + "id": "artifact_api~>192.0.2.70:443", + "sourceData": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + }, + "targetData": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.70:443", + "span.type": "external", + "id": ">192.0.2.70:443", + "label": ">192.0.2.70:443" + } + } + }, + { + "data": { + "id": "artifact_api", + "service.environment": "development", + "service.name": "artifact_api", + "agent.name": "nodejs", + "service.framework.name": "express" + } + }, + { + "data": { + "span.subtype": "http", + "span.destination.service.resource": "192.0.2.99:80", + "span.type": "external", + "id": ">192.0.2.99:80", + "label": ">192.0.2.99:80" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.186:443", + "span.type": "external", + "id": ">192.0.2.186:443", + "label": ">192.0.2.186:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.78:443", + "span.type": "external", + "id": ">192.0.2.78:443", + "label": ">192.0.2.78:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.226:443", + "span.type": "external", + "id": ">192.0.2.226:443", + "label": ">192.0.2.226:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.245:443", + "span.type": "external", + "id": ">192.0.2.245:443", + "label": ">192.0.2.245:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.77:443", + "span.type": "external", + "id": ">192.0.2.77:443", + "label": ">192.0.2.77:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.2:443", + "span.type": "external", + "id": ">192.0.2.2:443", + "label": ">192.0.2.2:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.198:443", + "span.type": "external", + "id": ">192.0.2.198:443", + "label": ">192.0.2.198:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.113:443", + "span.type": "external", + "id": ">192.0.2.113:443", + "label": ">192.0.2.113:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.39:443", + "span.type": "external", + "id": ">192.0.2.39:443", + "label": ">192.0.2.39:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.83:443", + "span.type": "external", + "id": ">192.0.2.83:443", + "label": ">192.0.2.83:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.5:443", + "span.type": "external", + "id": ">192.0.2.5:443", + "label": ">192.0.2.5:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.165:443", + "span.type": "external", + "id": ">192.0.2.165:443", + "label": ">192.0.2.165:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.156:443", + "span.type": "external", + "id": ">192.0.2.156:443", + "label": ">192.0.2.156:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.132:443", + "span.type": "external", + "id": ">192.0.2.132:443", + "label": ">192.0.2.132:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.240:443", + "span.type": "external", + "id": ">192.0.2.240:443", + "label": ">192.0.2.240:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.54:443", + "span.type": "external", + "id": ">192.0.2.54:443", + "label": ">192.0.2.54:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.213:443", + "span.type": "external", + "id": ">192.0.2.213:443", + "label": ">192.0.2.213:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.81:443", + "span.type": "external", + "id": ">192.0.2.81:443", + "label": ">192.0.2.81:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.176:443", + "span.type": "external", + "id": ">192.0.2.176:443", + "label": ">192.0.2.176:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.82:443", + "span.type": "external", + "id": ">192.0.2.82:443", + "label": ">192.0.2.82:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.23:443", + "span.type": "external", + "id": ">192.0.2.23:443", + "label": ">192.0.2.23:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.189:443", + "span.type": "external", + "id": ">192.0.2.189:443", + "label": ">192.0.2.189:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.190:443", + "span.type": "external", + "id": ">192.0.2.190:443", + "label": ">192.0.2.190:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.119:443", + "span.type": "external", + "id": ">192.0.2.119:443", + "label": ">192.0.2.119:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.169:443", + "span.type": "external", + "id": ">192.0.2.169:443", + "label": ">192.0.2.169:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.210:443", + "span.type": "external", + "id": ">192.0.2.210:443", + "label": ">192.0.2.210:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.148:443", + "span.type": "external", + "id": ">192.0.2.148:443", + "label": ">192.0.2.148:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.26:443", + "span.type": "external", + "id": ">192.0.2.26:443", + "label": ">192.0.2.26:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.139:443", + "span.type": "external", + "id": ">192.0.2.139:443", + "label": ">192.0.2.139:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.111:443", + "span.type": "external", + "id": ">192.0.2.111:443", + "label": ">192.0.2.111:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.13:443", + "span.type": "external", + "id": ">192.0.2.13:443", + "label": ">192.0.2.13:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.36:443", + "span.type": "external", + "id": ">192.0.2.36:443", + "label": ">192.0.2.36:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.69:443", + "span.type": "external", + "id": ">192.0.2.69:443", + "label": ">192.0.2.69:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.173:443", + "span.type": "external", + "id": ">192.0.2.173:443", + "label": ">192.0.2.173:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.144:443", + "span.type": "external", + "id": ">192.0.2.144:443", + "label": ">192.0.2.144:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.135:443", + "span.type": "external", + "id": ">192.0.2.135:443", + "label": ">192.0.2.135:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.21:443", + "span.type": "external", + "id": ">192.0.2.21:443", + "label": ">192.0.2.21:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.118:443", + "span.type": "external", + "id": ">192.0.2.118:443", + "label": ">192.0.2.118:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.42:443", + "span.type": "external", + "id": ">192.0.2.42:443", + "label": ">192.0.2.42:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.106:443", + "span.type": "external", + "id": ">192.0.2.106:443", + "label": ">192.0.2.106:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.3:443", + "span.type": "external", + "id": ">192.0.2.3:443", + "label": ">192.0.2.3:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.34:443", + "span.type": "external", + "id": ">192.0.2.34:443", + "label": ">192.0.2.34:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.185:443", + "span.type": "external", + "id": ">192.0.2.185:443", + "label": ">192.0.2.185:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.153:443", + "span.type": "external", + "id": ">192.0.2.153:443", + "label": ">192.0.2.153:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.9:443", + "span.type": "external", + "id": ">192.0.2.9:443", + "label": ">192.0.2.9:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.164:443", + "span.type": "external", + "id": ">192.0.2.164:443", + "label": ">192.0.2.164:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.47:443", + "span.type": "external", + "id": ">192.0.2.47:443", + "label": ">192.0.2.47:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.45:443", + "span.type": "external", + "id": ">192.0.2.45:443", + "label": ">192.0.2.45:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.8:443", + "span.type": "external", + "id": ">192.0.2.8:443", + "label": ">192.0.2.8:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.103:443", + "span.type": "external", + "id": ">192.0.2.103:443", + "label": ">192.0.2.103:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.60:443", + "span.type": "external", + "id": ">192.0.2.60:443", + "label": ">192.0.2.60:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.202:443", + "span.type": "external", + "id": ">192.0.2.202:443", + "label": ">192.0.2.202:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.70:443", + "span.type": "external", + "id": ">192.0.2.70:443", + "label": ">192.0.2.70:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.114:443", + "span.type": "external", + "id": ">192.0.2.114:443", + "label": ">192.0.2.114:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.25:443", + "span.type": "external", + "id": ">192.0.2.25:443", + "label": ">192.0.2.25:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.209:443", + "span.type": "external", + "id": ">192.0.2.209:443", + "label": ">192.0.2.209:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.248:443", + "span.type": "external", + "id": ">192.0.2.248:443", + "label": ">192.0.2.248:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.18:443", + "span.type": "external", + "id": ">192.0.2.18:443", + "label": ">192.0.2.18:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.107:443", + "span.type": "external", + "id": ">192.0.2.107:443", + "label": ">192.0.2.107:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.160:443", + "span.type": "external", + "id": ">192.0.2.160:443", + "label": ">192.0.2.160:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.228:443", + "span.type": "external", + "id": ">192.0.2.228:443", + "label": ">192.0.2.228:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.215:443", + "span.type": "external", + "id": ">192.0.2.215:443", + "label": ">192.0.2.215:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.162:443", + "span.type": "external", + "id": ">192.0.2.162:443", + "label": ">192.0.2.162:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.238:443", + "span.type": "external", + "id": ">192.0.2.238:443", + "label": ">192.0.2.238:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.145:443", + "span.type": "external", + "id": ">192.0.2.145:443", + "label": ">192.0.2.145:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.239:443", + "span.type": "external", + "id": ">192.0.2.239:443", + "label": ">192.0.2.239:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.59:443", + "span.type": "external", + "id": ">192.0.2.59:443", + "label": ">192.0.2.59:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.71:443", + "span.type": "external", + "id": ">192.0.2.71:443", + "label": ">192.0.2.71:443" + } + }, + { + "data": { + "span.subtype": "https", + "span.destination.service.resource": "192.0.2.48:443", + "span.type": "external", + "id": ">192.0.2.48:443", + "label": ">192.0.2.48:443" + } + }, + { + "data": { + "service.name": "graphics-worker", + "agent.name": "nodejs", + "service.environment": null, + "service.framework.name": null, + "id": "graphics-worker" + } + } + ] +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx index 7c69fb28d668f..920ef39e84ca3 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx @@ -21,13 +21,13 @@ import { ApmPluginContext, ApmPluginContextValue, } from '../../../../../context/ApmPluginContext'; +import { EuiThemeProvider } from '../../../../../../../observability/public'; storiesOf( 'app/Settings/AgentConfigurations/AgentConfigurationCreateEdit', module -).add( - 'with config', - () => { +) + .addDecorator((storyFn) => { const httpMock = {}; // mock @@ -40,10 +40,21 @@ storiesOf( }, }, }; + return ( - + + + {storyFn()} + + + ); + }) + .add( + 'with config', + () => { + return ( - - ); - }, - { - info: { - source: false, + ); }, - } -); + { + info: { + source: false, + }, + } + ); From 9ce10b732a662e05820b423941d07455192d7146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Wed, 24 Jun 2020 16:08:27 +0100 Subject: [PATCH 02/82] adding Stats interface with type (#69784) (#69795) --- .../typings/fetch_data_response/index.d.ts | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/observability/public/typings/fetch_data_response/index.d.ts b/x-pack/plugins/observability/public/typings/fetch_data_response/index.d.ts index 30ecb24a58a5a..06e86d1096cfc 100644 --- a/x-pack/plugins/observability/public/typings/fetch_data_response/index.d.ts +++ b/x-pack/plugins/observability/public/typings/fetch_data_response/index.d.ts @@ -4,17 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -interface Percentage { - label: string; - pct: number; - color?: string; -} -interface Bytes { - label: string; - bytes: number; - color?: string; -} -interface Numeral { +interface Stat { + type: 'number' | 'percent' | 'bytesPerSecond'; label: string; value: number; color?: string; @@ -37,18 +28,18 @@ export interface FetchDataResponse { } export interface LogsFetchDataResponse extends FetchDataResponse { - stats: Record; + stats: Record; series: Record; } export interface MetricsFetchDataResponse extends FetchDataResponse { stats: { - hosts: Numeral; - cpu: Percentage; - memory: Percentage; - disk: Percentage; - inboundTraffic: Bytes; - outboundTraffic: Bytes; + hosts: Stat; + cpu: Stat; + memory: Stat; + disk: Stat; + inboundTraffic: Stat; + outboundTraffic: Stat; }; series: { inboundTraffic: Series; @@ -58,9 +49,9 @@ export interface MetricsFetchDataResponse extends FetchDataResponse { export interface UptimeFetchDataResponse extends FetchDataResponse { stats: { - monitors: Numeral; - up: Numeral; - down: Numeral; + monitors: Stat; + up: Stat; + down: Stat; }; series: { up: Series; @@ -70,8 +61,8 @@ export interface UptimeFetchDataResponse extends FetchDataResponse { export interface ApmFetchDataResponse extends FetchDataResponse { stats: { - services: Numeral; - transactions: Numeral; + services: Stat; + transactions: Stat; }; series: { transactions: Series; From 15ad35485415eb76424cfaf8ab1408e95a7dddca Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Wed, 24 Jun 2020 10:18:39 -0500 Subject: [PATCH 03/82] [7.x] [chore] TS 3.9: convert ts-ignore to ts-expect-error (#69541) (#69771) Co-authored-by: Elastic Machine --- .../functions/browser/markdown.ts | 2 +- .../functions/common/compare.ts | 8 +++--- .../functions/common/containerStyle.ts | 2 +- .../functions/common/image.ts | 4 +-- .../functions/common/math.ts | 3 +-- .../functions/common/palette.ts | 2 +- .../canvas_plugin_src/functions/common/pie.ts | 6 ++--- .../functions/common/plot/index.ts | 6 ++--- .../functions/common/render.ts | 1 - .../functions/common/repeatImage.ts | 4 +-- .../functions/common/revealImage.ts | 4 +-- .../functions/common/saved_visualization.ts | 2 +- .../functions/common/staticColumn.ts | 1 - .../functions/server/demodata/index.ts | 2 +- .../functions/server/escount.ts | 2 +- .../functions/server/esdocs.ts | 2 +- .../functions/server/essql.ts | 2 +- .../functions/server/get_field_names.test.ts | 2 +- .../functions/server/pointseries/index.ts | 11 +++----- .../pointseries/lib/is_column_reference.ts | 2 +- .../canvas/canvas_plugin_src/plugin.ts | 9 +++---- .../input_type_to_expression/visualization.ts | 2 +- .../uis/arguments/date_format/index.ts | 1 - .../canvas_plugin_src/uis/arguments/index.ts | 24 ++++++++--------- .../uis/arguments/number_format/index.ts | 1 - .../canvas_plugin_src/uis/views/index.ts | 27 +++++++++---------- .../plugins/canvas/common/lib/autocomplete.ts | 2 +- x-pack/plugins/canvas/common/lib/dataurl.ts | 2 +- x-pack/plugins/canvas/common/lib/index.ts | 26 +++++++----------- .../common/lib/pivot_object_array.test.ts | 2 +- x-pack/plugins/canvas/public/application.tsx | 10 +++---- .../export/__tests__/export_app.test.tsx | 2 +- .../workpad/workpad_app/workpad_telemetry.tsx | 3 --- .../arg_add_popover/arg_add_popover.tsx | 6 ++--- .../public/components/asset_manager/asset.tsx | 1 - .../components/asset_manager/asset_modal.tsx | 2 -- .../public/components/asset_manager/index.ts | 8 +++--- .../components/asset_picker/asset_picker.tsx | 9 +------ .../custom_element_modal.tsx | 2 -- .../components/embeddable_flyout/index.tsx | 2 +- .../components/file_upload/file_upload.tsx | 1 - .../components/font_picker/font_picker.tsx | 1 - .../canvas/public/components/router/index.ts | 4 +-- .../components/saved_elements_modal/index.ts | 5 ++-- .../element_settings/element_settings.tsx | 4 +-- .../components/sidebar/global_config.tsx | 7 +++-- .../public/components/sidebar/sidebar.tsx | 2 +- .../public/components/toolbar/toolbar.tsx | 8 +++--- .../workpad_header/edit_menu/index.ts | 12 ++++----- .../element_menu/element_menu.tsx | 1 - .../workpad_header/element_menu/index.tsx | 4 +-- .../fullscreen_control/fullscreen_control.tsx | 2 +- .../components/workpad_header/index.tsx | 3 --- .../workpad_header/refresh_control/index.ts | 3 +-- .../workpad_header/share_menu/flyout/index.ts | 1 - .../workpad_header/share_menu/utils.ts | 1 - .../workpad_header/view_menu/index.ts | 4 +-- .../workpad_header/workpad_header.tsx | 5 ++-- .../interaction_boundary.tsx | 2 +- .../workpad_shortcuts/workpad_shortcuts.tsx | 2 +- .../extended_template.examples.tsx | 2 +- .../__examples__/simple_template.examples.tsx | 2 +- .../__examples__/simple_template.examples.tsx | 2 +- .../plugins/canvas/public/functions/asset.ts | 2 +- .../canvas/public/functions/filters.ts | 2 +- .../canvas/public/functions/timelion.ts | 2 +- x-pack/plugins/canvas/public/functions/to.ts | 2 +- x-pack/plugins/canvas/public/lib/app_state.ts | 6 ++--- .../public/lib/build_embeddable_filters.ts | 2 +- .../canvas/public/lib/clipboard.test.ts | 2 +- .../canvas/public/lib/clone_subgraphs.ts | 2 +- .../plugins/canvas/public/lib/create_thunk.ts | 2 +- .../canvas/public/lib/download_workpad.ts | 2 +- .../public/lib/element_handler_creators.ts | 2 -- .../plugins/canvas/public/lib/es_service.ts | 1 - .../public/lib/sync_filter_expression.ts | 1 - x-pack/plugins/canvas/public/plugin.tsx | 2 +- x-pack/plugins/canvas/public/registries.ts | 10 +++---- .../canvas/public/state/actions/embeddable.ts | 2 +- .../canvas/public/state/actions/workpad.ts | 2 +- .../__tests__/workpad_autoplay.test.ts | 2 +- .../__tests__/workpad_refresh.test.ts | 1 - .../public/state/middleware/in_flight.ts | 2 +- .../state/middleware/workpad_autoplay.ts | 4 +-- .../state/middleware/workpad_refresh.ts | 5 ++-- .../public/state/reducers/embeddable.ts | 2 +- .../public/state/selectors/resolved_args.ts | 4 +-- .../canvas/public/state/selectors/workpad.ts | 4 +-- x-pack/plugins/canvas/public/store.ts | 4 +-- .../server/routes/es_fields/es_fields.ts | 2 +- .../server/routes/shareables/download.ts | 1 - .../server/sample_data/load_sample_data.ts | 5 ++-- .../api/__tests__/shareable.test.tsx | 2 +- .../rendered_element.examples.tsx | 2 +- .../footer/settings/autoplay_settings.tsx | 1 - .../components/rendered_element.tsx | 6 ++--- .../shareable_runtime/supported_renderers.js | 4 --- .../plugins/canvas/shareable_runtime/types.ts | 2 +- 98 files changed, 164 insertions(+), 220 deletions(-) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts index 41323a82f4ee0..e44fb903ef042 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/markdown.ts @@ -10,7 +10,7 @@ import { Style, ExpressionFunctionDefinition, } from 'src/plugins/expressions/common'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { Handlebars } from '../../../common/lib/handlebars'; import { getFunctionHelp } from '../../../i18n'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/compare.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/compare.ts index e952faca1d5eb..8a28f71ee1b47 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/compare.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/compare.ts @@ -59,25 +59,25 @@ export function compare(): ExpressionFunctionDefinition<'compare', Context, Argu return a !== b; case Operation.LT: if (typesMatch) { - // @ts-ignore #35433 This is a wonky comparison for nulls + // @ts-expect-error #35433 This is a wonky comparison for nulls return a < b; } return false; case Operation.LTE: if (typesMatch) { - // @ts-ignore #35433 This is a wonky comparison for nulls + // @ts-expect-error #35433 This is a wonky comparison for nulls return a <= b; } return false; case Operation.GT: if (typesMatch) { - // @ts-ignore #35433 This is a wonky comparison for nulls + // @ts-expect-error #35433 This is a wonky comparison for nulls return a > b; } return false; case Operation.GTE: if (typesMatch) { - // @ts-ignore #35433 This is a wonky comparison for nulls + // @ts-expect-error #35433 This is a wonky comparison for nulls return a >= b; } return false; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts index b841fde284ab6..09ce2b2bf1755 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/containerStyle.ts @@ -6,7 +6,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { ContainerStyle, Overflow, BackgroundRepeat, BackgroundSize } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { isValidUrl } from '../../../common/lib/url'; interface Output extends ContainerStyle { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts index c43ff6373ea0f..3ef956b41ce20 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/image.ts @@ -6,9 +6,9 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl'; -// @ts-ignore .png file +// @ts-expect-error .png file import { elasticLogo } from '../../lib/elastic_logo'; export enum ImageMode { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts index 7f84dc54d8092..e36644530eae8 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore no @typed def; Elastic library +// @ts-expect-error no @typed def; Elastic library import { evaluate } from 'tinymath'; -// @ts-ignore untyped local import { pivotObjectArray } from '../../../common/lib/pivot_object_array'; import { Datatable, isDatatable, ExpressionFunctionDefinition } from '../../../types'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.ts index 63cd663d2ac4c..f27abe261e2e2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.ts @@ -5,7 +5,7 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { palettes } from '../../../common/lib/palettes'; import { getFunctionHelp } from '../../../i18n'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts index 6cb64a43ea582..b568f18924869 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/pie.ts @@ -5,11 +5,11 @@ */ import { get, map, groupBy } from 'lodash'; -// @ts-ignore lodash.keyby imports invalid member from @types/lodash +// @ts-expect-error lodash.keyby imports invalid member from @types/lodash import keyBy from 'lodash.keyby'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { getColorsFromPalette } from '../../../common/lib/get_colors_from_palette'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { getLegendConfig } from '../../../common/lib/get_legend_config'; import { getFunctionHelp } from '../../../i18n'; import { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts index e8214ca8eaf9f..0b4583f4581ae 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore no @typed def +// @ts-expect-error no @typed def import keyBy from 'lodash.keyby'; import { groupBy, get, set, map, sortBy } from 'lodash'; import { ExpressionFunctionDefinition, Style } from 'src/plugins/expressions'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { getColorsFromPalette } from '../../../../common/lib/get_colors_from_palette'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { getLegendConfig } from '../../../../common/lib/get_legend_config'; import { getFlotAxisConfig } from './get_flot_axis_config'; import { getFontSpec } from './get_font_spec'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.ts index da50195480c68..f8eeabfccde6d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/render.ts @@ -7,7 +7,6 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Render, ContainerStyle } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; -// @ts-ignore unconverted local file import { DEFAULT_ELEMENT_CSS } from '../../../common/lib/constants'; interface ContainerStyleArgument extends ContainerStyle { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeatImage.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeatImage.ts index f91fd3dfc5522..9e296f2b9a92a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeatImage.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/repeatImage.ts @@ -5,9 +5,9 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl'; -// @ts-ignore .png file +// @ts-expect-error .png file import { elasticOutline } from '../../lib/elastic_outline'; import { Render } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts index d961227a302b8..3e721cc49b411 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/revealImage.ts @@ -5,9 +5,9 @@ */ import { ExpressionFunctionDefinition, ExpressionValueRender } from 'src/plugins/expressions'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { resolveWithMissingImage } from '../../../common/lib/resolve_dataurl'; -// @ts-ignore .png file +// @ts-expect-error .png file import { elasticOutline } from '../../lib/elastic_outline'; import { getFunctionHelp, getFunctionErrors } from '../../../i18n'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts index 62328556b45e3..da441bc3266c1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts @@ -79,7 +79,7 @@ export function savedVisualization(): ExpressionFunctionDefinition< } if (hideLegend === true) { - // @ts-ignore LegendOpen missing on VisualizeInput + // @ts-expect-error LegendOpen missing on VisualizeInput visOptions.legendOpen = false; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts index 9dd38dd57c677..4fa4be0a2f09f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore untyped Elastic library import { getType } from '@kbn/interpreter/common'; import { ExpressionFunctionDefinition, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts index 843e2bda47e12..60d5edeb10483 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts @@ -6,7 +6,7 @@ import { sortBy } from 'lodash'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; -// @ts-ignore unconverted lib file +// @ts-expect-error unconverted lib file import { queryDatatable } from '../../../../common/lib/datatable/query'; import { DemoRows } from './demo_rows_types'; import { getDemoRows } from './get_demo_rows'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/escount.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/escount.ts index 142331aabf351..26f651e770363 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/escount.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/escount.ts @@ -9,7 +9,7 @@ import { ExpressionValueFilter, } from 'src/plugins/expressions/common'; /* eslint-disable */ -// @ts-ignore untyped local +// @ts-expect-error untyped local import { buildESRequest } from '../../../server/lib/build_es_request'; /* eslint-enable */ import { getFunctionHelp } from '../../../i18n'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts index 2b229b8957ec1..a090f09a76ea2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/esdocs.ts @@ -7,7 +7,7 @@ import squel from 'squel'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions'; /* eslint-disable */ -// @ts-ignore untyped local +// @ts-expect-error untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; /* eslint-enable */ import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts index c64398d4b3a18..5ac91bec849c2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/essql.ts @@ -6,7 +6,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; /* eslint-disable */ -// @ts-ignore untyped local +// @ts-expect-error untyped local import { queryEsSQL } from '../../../server/lib/query_es_sql'; /* eslint-enable */ import { ExpressionValueFilter } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts index c25628e5cf2b9..7dee587895485 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore untyped library +// @ts-expect-error untyped library import { parse } from 'tinymath'; import { getFieldNames } from './pointseries/lib/get_field_names'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts index 54e48c8abf04b..bae80d3c33510 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore Untyped library +// @ts-expect-error untyped library import uniqBy from 'lodash.uniqby'; -// @ts-ignore Untyped Elastic library +// @ts-expect-error untyped Elastic library import { evaluate } from 'tinymath'; import { groupBy, zipObject, omit } from 'lodash'; import moment from 'moment'; @@ -18,13 +18,10 @@ import { PointSeriesColumnName, PointSeriesColumns, } from 'src/plugins/expressions/common'; -// @ts-ignore Untyped local import { pivotObjectArray } from '../../../../common/lib/pivot_object_array'; -// @ts-ignore Untyped local import { unquoteString } from '../../../../common/lib/unquote_string'; -// @ts-ignore Untyped local import { isColumnReference } from './lib/is_column_reference'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { getExpressionType } from './lib/get_expression_type'; import { getFunctionHelp, getFunctionErrors } from '../../../../i18n'; @@ -125,7 +122,7 @@ export function pointseries(): ExpressionFunctionDefinition< col.role = 'measure'; } - // @ts-ignore untyped local: get_expression_type + // @ts-expect-error untyped local: get_expression_type columns[argName] = col; } }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts index 0ecc135ba9042..aed9861e1250c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore Untyped Library +// @ts-expect-error untyped library import { parse } from 'tinymath'; export function isColumnReference(mathExpression: string | null): boolean { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts index c9ce4d065968a..4fbb5d0069e51 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts @@ -12,17 +12,16 @@ import { Start as InspectorStart } from '../../../../src/plugins/inspector/publi import { functions } from './functions/browser'; import { typeFunctions } from './expression_types'; -// @ts-ignore: untyped local +// @ts-expect-error: untyped local import { renderFunctions, renderFunctionFactories } from './renderers'; import { initializeElements } from './elements'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { transformSpecs } from './uis/transforms'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { datasourceSpecs } from './uis/datasources'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { modelSpecs } from './uis/models'; import { initializeViews } from './uis/views'; -// @ts-ignore Untyped Local import { initializeArgs } from './uis/arguments'; import { tagSpecs } from './uis/tags'; import { templateSpecs } from './templates'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts index 4c8de2afd81ad..f03c10e2d424e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts @@ -26,7 +26,7 @@ export function toExpression(input: VisualizeInput): string { .reduce((_, part) => expressionParts.push(part), 0); } - // @ts-ignore LegendOpen missing on VisualizeInput type + // @ts-expect-error LegendOpen missing on VisualizeInput type if (input.vis?.legendOpen !== undefined && input.vis.legendOpen === false) { expressionParts.push(`hideLegend=true`); } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts index fce9b21fa0387..e972928fe20b0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts @@ -7,7 +7,6 @@ import { compose, withProps } from 'recompose'; import moment from 'moment'; import { DateFormatArgInput as Component, Props as ComponentProps } from './date_format'; -// @ts-ignore untyped local lib import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts index 2f9a21d8a009f..94a9cf28aef69 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts @@ -5,31 +5,31 @@ */ import { axisConfig } from './axis_config'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { datacolumn } from './datacolumn'; import { dateFormatInitializer } from './date_format'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { filterGroup } from './filter_group'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { imageUpload } from './image_upload'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { number } from './number'; import { numberFormatInitializer } from './number_format'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { palette } from './palette'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { percentage } from './percentage'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { range } from './range'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { select } from './select'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { shape } from './shape'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { string } from './string'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { textarea } from './textarea'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { toggle } from './toggle'; import { SetupInitializer } from '../../plugin'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts index 5a3e3904f4f23..17d630f0ab9e2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts @@ -6,7 +6,6 @@ import { compose, withProps } from 'recompose'; import { NumberFormatArgInput as Component, Props as ComponentProps } from './number_format'; -// @ts-ignore untyped local lib import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/index.ts index 34877f2fd551b..19f10628a90cb 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/views/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/views/index.ts @@ -4,33 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore untyped local +// @ts-expect-error untyped local import { dropdownControl } from './dropdownControl'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { getCell } from './getCell'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { image } from './image'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { markdown } from './markdown'; -// @ts-ignore untyped local import { metricInitializer } from './metric'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { pie } from './pie'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { plot } from './plot'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { progress } from './progress'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { repeatImage } from './repeatImage'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { revealImage } from './revealImage'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { render } from './render'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { shape } from './shape'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { table } from './table'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { timefilterControl } from './timefilterControl'; import { SetupInitializer } from '../../plugin'; diff --git a/x-pack/plugins/canvas/common/lib/autocomplete.ts b/x-pack/plugins/canvas/common/lib/autocomplete.ts index c97879de2137e..0a30b2e2f598e 100644 --- a/x-pack/plugins/canvas/common/lib/autocomplete.ts +++ b/x-pack/plugins/canvas/common/lib/autocomplete.ts @@ -5,7 +5,7 @@ */ import { uniq } from 'lodash'; -// @ts-ignore Untyped Library +// @ts-expect-error untyped library import { parse } from '@kbn/interpreter/common'; import { ExpressionAstExpression, diff --git a/x-pack/plugins/canvas/common/lib/dataurl.ts b/x-pack/plugins/canvas/common/lib/dataurl.ts index ea5a26b27e423..60e65a6d3ca1c 100644 --- a/x-pack/plugins/canvas/common/lib/dataurl.ts +++ b/x-pack/plugins/canvas/common/lib/dataurl.ts @@ -6,7 +6,7 @@ import { fromByteArray } from 'base64-js'; -// @ts-ignore @types/mime doesn't resolve mime/lite for some reason. +// @ts-expect-error @types/mime doesn't resolve mime/lite for some reason. import mime from 'mime/lite'; const dataurlRegex = /^data:([a-z]+\/[a-z0-9-+.]+)(;[a-z-]+=[a-z0-9-]+)?(;([a-z0-9]+))?,/; diff --git a/x-pack/plugins/canvas/common/lib/index.ts b/x-pack/plugins/canvas/common/lib/index.ts index 5ab29c290c3da..4cb3cbbb9b4e6 100644 --- a/x-pack/plugins/canvas/common/lib/index.ts +++ b/x-pack/plugins/canvas/common/lib/index.ts @@ -4,39 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore missing local definition +// @ts-expect-error missing local definition export * from './datatable'; -// @ts-ignore missing local definition export * from './autocomplete'; export * from './constants'; export * from './dataurl'; -// @ts-ignore missing local definition +// @ts-expect-error missing local definition export * from './errors'; -// @ts-ignore missing local definition +// @ts-expect-error missing local definition export * from './expression_form_handlers'; -// @ts-ignore missing local definition export * from './fetch'; export * from './fonts'; -// @ts-ignore missing local definition +// @ts-expect-error missing local definition export * from './get_colors_from_palette'; -// @ts-ignore missing local definition export * from './get_field_type'; -// @ts-ignore missing local definition +// @ts-expect-error missing local definition export * from './get_legend_config'; -// @ts-ignore missing local definition +// @ts-expect-error missing local definition export * from './handlebars'; export * from './hex_to_rgb'; -// @ts-ignore missing local definition export * from './httpurl'; -// @ts-ignore missing local definition +// @ts-expect-error missing local definition export * from './missing_asset'; -// @ts-ignore missing local definition +// @ts-expect-error missing local definition export * from './palettes'; -// @ts-ignore missing local definition export * from './pivot_object_array'; -// @ts-ignore missing local definition +// @ts-expect-error missing local definition export * from './resolve_dataurl'; -// @ts-ignore missing local definition export * from './unquote_string'; -// @ts-ignore missing local definition +// @ts-expect-error missing local definition export * from './url'; diff --git a/x-pack/plugins/canvas/common/lib/pivot_object_array.test.ts b/x-pack/plugins/canvas/common/lib/pivot_object_array.test.ts index faf319769cab0..0fbc2fa6b0f38 100644 --- a/x-pack/plugins/canvas/common/lib/pivot_object_array.test.ts +++ b/x-pack/plugins/canvas/common/lib/pivot_object_array.test.ts @@ -55,7 +55,7 @@ describe('pivotObjectArray', () => { }); it('throws when given an invalid column list', () => { - // @ts-ignore testing potential calls from legacy code that should throw + // @ts-expect-error testing potential calls from legacy code that should throw const check = () => pivotObjectArray(rows, [{ name: 'price' }, { name: 'missing' }]); expect(check).toThrowError('Columns should be an array of strings'); }); diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index c799f36a283c1..b2c836fe4805f 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -15,14 +15,14 @@ import { BehaviorSubject } from 'rxjs'; import { AppMountParameters, CoreStart, CoreSetup, AppUpdater } from 'kibana/public'; import { CanvasStartDeps, CanvasSetupDeps } from './plugin'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { App } from './components/app'; import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; import { registerLanguage } from './lib/monaco_language_def'; import { SetupRegistries } from './plugin_api'; import { initRegistries, populateRegistries, destroyRegistries } from './registries'; import { getDocumentationLinks } from './lib/documentation_links'; -// @ts-ignore untyped component +// @ts-expect-error untyped component import { HelpMenu } from './components/help_menu/help_menu'; import { createStore } from './store'; @@ -32,12 +32,12 @@ import { init as initStatsReporter } from './lib/ui_metric'; import { CapabilitiesStrings } from '../i18n'; import { startServices, services } from './services'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { createHistory, destroyHistory } from './lib/history_provider'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { stopRouter } from './lib/router_provider'; import { initFunctions } from './functions'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { appUnload } from './state/actions/app'; import './style/index.scss'; diff --git a/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx b/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx index 7f5b53df4ba52..b0a8d1e990e75 100644 --- a/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx +++ b/x-pack/plugins/canvas/public/apps/export/export/__tests__/export_app.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { mount } from 'enzyme'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { ExportApp } from '../export_app'; jest.mock('style-it', () => ({ diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx index 3014369d94857..981334ff8d9f2 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx +++ b/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx @@ -6,11 +6,8 @@ import React, { useState, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -// @ts-ignore: Local Untyped import { trackCanvasUiMetric, METRIC_TYPE } from '../../../lib/ui_metric'; -// @ts-ignore: Local Untyped import { getElementCounts } from '../../../state/selectors/workpad'; -// @ts-ignore: Local Untyped import { getArgs } from '../../../state/selectors/resolved_args'; const WorkpadLoadedMetric = 'workpad-loaded'; diff --git a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx index c26fdb8c46d0f..26295acecd920 100644 --- a/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx +++ b/x-pack/plugins/canvas/public/components/arg_add_popover/arg_add_popover.tsx @@ -7,11 +7,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EuiButtonIcon } from '@elastic/eui'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { Popover, PopoverChildrenProps } from '../popover'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { ArgAdd } from '../arg_add'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { Arg } from '../../expression_types/arg'; import { ComponentStrings } from '../../../i18n'; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset.tsx index cb7ec1aba8f59..b0eaecc7b5203 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset.tsx @@ -7,7 +7,6 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, - // @ts-ignore (elastic/eui#1262) EuiImage is not exported yet EuiImage, EuiPanel, EuiSpacer, diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_modal.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset_modal.tsx index bf34f60ce75b3..ebc398a805b62 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset_modal.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_modal.tsx @@ -6,7 +6,6 @@ import { EuiButton, EuiEmptyPrompt, - // @ts-ignore (elastic/eui#1557) EuiFilePicker is not exported yet EuiFilePicker, EuiFlexGrid, EuiFlexGroup, @@ -26,7 +25,6 @@ import React, { FunctionComponent } from 'react'; import { ComponentStrings } from '../../../i18n'; -// @ts-ignore import { ASSET_MAX_SIZE } from '../../../common/lib/constants'; import { Loading } from '../loading'; import { Asset } from './asset'; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/index.ts b/x-pack/plugins/canvas/public/components/asset_manager/index.ts index 23dbe3df085d4..b07857f13f6c6 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/index.ts +++ b/x-pack/plugins/canvas/public/components/asset_manager/index.ts @@ -9,16 +9,16 @@ import { compose, withProps } from 'recompose'; import { set, get } from 'lodash'; import { fromExpression, toExpression } from '@kbn/interpreter/common'; import { getAssets } from '../../state/selectors/assets'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { removeAsset, createAsset } from '../../state/actions/assets'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { elementsRegistry } from '../../lib/elements_registry'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { addElement } from '../../state/actions/elements'; import { getSelectedPage } from '../../state/selectors/workpad'; import { encode } from '../../../common/lib/dataurl'; import { getId } from '../../lib/get_id'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { findExistingAsset } from '../../lib/find_existing_asset'; import { VALID_IMAGE_TYPES } from '../../../common/lib/constants'; import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx b/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx index 4489e877abf88..1f49e9ae14f5c 100644 --- a/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx +++ b/x-pack/plugins/canvas/public/components/asset_picker/asset_picker.tsx @@ -6,14 +6,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { - EuiFlexGrid, - EuiFlexItem, - EuiLink, - // @ts-ignore (elastic/eui#1557) EuiImage is not exported yet - EuiImage, - EuiIcon, -} from '@elastic/eui'; +import { EuiFlexGrid, EuiFlexItem, EuiLink, EuiImage, EuiIcon } from '@elastic/eui'; import { CanvasAsset } from '../../../types'; diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx b/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx index 8f73939de69a6..ceb7c83f3cab5 100644 --- a/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx +++ b/x-pack/plugins/canvas/public/components/custom_element_modal/custom_element_modal.tsx @@ -12,7 +12,6 @@ import { EuiButton, EuiButtonEmpty, EuiFieldText, - // @ts-ignore hasn't been converted to TypeScript yet EuiFilePicker, EuiFlexGroup, EuiFlexItem, @@ -27,7 +26,6 @@ import { EuiTextArea, EuiTitle, } from '@elastic/eui'; -// @ts-ignore converting /libs/constants to TS breaks CI import { VALID_IMAGE_TYPES } from '../../../common/lib/constants'; import { encode } from '../../../common/lib/dataurl'; import { ElementCard } from '../element_card'; diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx index 8e69396f67c2e..9462ba0411de4 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/index.tsx @@ -10,7 +10,7 @@ import { compose } from 'recompose'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { AddEmbeddableFlyout, Props } from './flyout'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { addElement } from '../../state/actions/elements'; import { getSelectedPage } from '../../state/selectors/workpad'; import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; diff --git a/x-pack/plugins/canvas/public/components/file_upload/file_upload.tsx b/x-pack/plugins/canvas/public/components/file_upload/file_upload.tsx index 993ee8bde2653..22fa32606407b 100644 --- a/x-pack/plugins/canvas/public/components/file_upload/file_upload.tsx +++ b/x-pack/plugins/canvas/public/components/file_upload/file_upload.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore (elastic/eui#1262) EuiFilePicker is not exported yet import { EuiFilePicker } from '@elastic/eui'; import PropTypes from 'prop-types'; import React, { FunctionComponent } from 'react'; diff --git a/x-pack/plugins/canvas/public/components/font_picker/font_picker.tsx b/x-pack/plugins/canvas/public/components/font_picker/font_picker.tsx index 4340430829342..556a3c5452160 100644 --- a/x-pack/plugins/canvas/public/components/font_picker/font_picker.tsx +++ b/x-pack/plugins/canvas/public/components/font_picker/font_picker.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore (elastic/eui#1262) EuiSuperSelect is not exported yet import { EuiSuperSelect } from '@elastic/eui'; import PropTypes from 'prop-types'; import React, { FunctionComponent } from 'react'; diff --git a/x-pack/plugins/canvas/public/components/router/index.ts b/x-pack/plugins/canvas/public/components/router/index.ts index fa857c6f0cd3c..561ad0e9401f5 100644 --- a/x-pack/plugins/canvas/public/components/router/index.ts +++ b/x-pack/plugins/canvas/public/components/router/index.ts @@ -5,14 +5,14 @@ */ import { connect } from 'react-redux'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { setFullscreen } from '../../state/actions/transient'; import { enableAutoplay, setRefreshInterval, setAutoplayInterval, } from '../../state/actions/workpad'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { Router as Component } from './router'; import { State } from '../../../types'; diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts index f14fc92e028db..c5c1dbc2fdd6e 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/index.ts @@ -8,14 +8,13 @@ import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { compose, withState } from 'recompose'; import { camelCase } from 'lodash'; -// @ts-ignore Untyped local import { cloneSubgraphs } from '../../lib/clone_subgraphs'; import * as customElementService from '../../lib/custom_element_service'; import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; import { WithKibanaProps } from '../../'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { selectToplevelNodes } from '../../state/actions/transient'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { insertNodes } from '../../state/actions/elements'; import { getSelectedPage } from '../../state/selectors/workpad'; import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; diff --git a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx index 74f4887601d30..e3f4e00f4de01 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/element_settings/element_settings.tsx @@ -7,9 +7,9 @@ import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; import { EuiTabbedContent } from '@elastic/eui'; -// @ts-ignore unconverted component +// @ts-expect-error unconverted component import { Datasource } from '../../datasource'; -// @ts-ignore unconverted component +// @ts-expect-error unconverted component import { FunctionFormList } from '../../function_form_list'; import { PositionedElement } from '../../../../types'; import { ComponentStrings } from '../../../../i18n'; diff --git a/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx b/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx index 2e241681ccc6a..f89ab79a086cf 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/global_config.tsx @@ -5,13 +5,12 @@ */ import React, { Fragment, FunctionComponent } from 'react'; -// @ts-ignore unconverted component +// @ts-expect-error unconverted component import { ElementConfig } from '../element_config'; -// @ts-ignore unconverted component +// @ts-expect-error unconverted component import { PageConfig } from '../page_config'; -// @ts-ignore unconverted component import { WorkpadConfig } from '../workpad_config'; -// @ts-ignore unconverted component +// @ts-expect-error unconverted component import { SidebarSection } from './sidebar_section'; export const GlobalConfig: FunctionComponent = () => ( diff --git a/x-pack/plugins/canvas/public/components/sidebar/sidebar.tsx b/x-pack/plugins/canvas/public/components/sidebar/sidebar.tsx index 26f106911e015..9f1936fdc143b 100644 --- a/x-pack/plugins/canvas/public/components/sidebar/sidebar.tsx +++ b/x-pack/plugins/canvas/public/components/sidebar/sidebar.tsx @@ -5,7 +5,7 @@ */ import React, { FunctionComponent } from 'react'; -// @ts-ignore unconverted component +// @ts-expect-error unconverted component import { SidebarContent } from './sidebar_content'; interface Props { diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx index 0f8204e6bc261..9a26b438e17c3 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.tsx @@ -20,13 +20,13 @@ import { CanvasElement } from '../../../types'; import { ComponentStrings } from '../../../i18n'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { Navbar } from '../navbar'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { WorkpadManager } from '../workpad_manager'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { PageManager } from '../page_manager'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { Expression } from '../expression'; import { Tray } from './tray'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts index 75bdcd2b0ada1..8f013f70aefcd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/index.ts @@ -9,17 +9,17 @@ import { compose, withHandlers, withProps } from 'recompose'; import { Dispatch } from 'redux'; import { State, PositionedElement } from '../../../../types'; import { getClipboardData } from '../../../lib/clipboard'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { flatten } from '../../../lib/aeroelastic/functional'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { globalStateUpdater } from '../../workpad_page/integration_utils'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { crawlTree } from '../../workpad_page/integration_utils'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { undoHistory, redoHistory } from '../../../state/actions/history'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { selectToplevelNodes } from '../../../state/actions/transient'; import { getSelectedPage, diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx index fbb5d70dfc55c..6d9233aaba22b 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx @@ -19,7 +19,6 @@ import { ElementSpec } from '../../../../types'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { getId } from '../../../lib/get_id'; import { Popover, ClosePopoverFn } from '../../popover'; -// @ts-ignore Untyped local import { AssetManager } from '../../asset_manager'; import { SavedElementsModal } from '../../saved_elements_modal'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx index a1227b3394678..13b2cace13a40 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.tsx @@ -10,10 +10,10 @@ import { compose, withProps } from 'recompose'; import { Dispatch } from 'redux'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public/'; import { State, ElementSpec } from '../../../../types'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { elementsRegistry } from '../../../lib/elements_registry'; import { ElementMenu as Component, Props as ComponentProps } from './element_menu'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { addElement } from '../../../state/actions/elements'; import { getSelectedPage } from '../../../state/selectors/workpad'; import { AddEmbeddablePanel } from '../../embeddable_flyout'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.tsx b/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.tsx index 5ffa712abee13..77edf9d2264d4 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.tsx @@ -6,7 +6,7 @@ import React, { ReactNode, KeyboardEvent } from 'react'; import PropTypes from 'prop-types'; -// @ts-ignore no @types definition +// @ts-expect-error no @types definition import { Shortcuts } from 'react-shortcuts'; import { isTextInput } from '../../../lib/is_text_input'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/index.tsx b/x-pack/plugins/canvas/public/components/workpad_header/index.tsx index d2fece567a8ad..407b4ff932811 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/index.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/index.tsx @@ -6,11 +6,8 @@ import { connect } from 'react-redux'; import { Dispatch } from 'redux'; -// @ts-ignore untyped local import { canUserWrite } from '../../state/selectors/app'; -// @ts-ignore untyped local import { getSelectedPage, isWriteable } from '../../state/selectors/workpad'; -// @ts-ignore untyped local import { setWriteable } from '../../state/actions/workpad'; import { State } from '../../../types'; import { WorkpadHeader as Component, Props as ComponentProps } from './workpad_header'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts index 53c053811a273..87b926d93ccb9 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/refresh_control/index.ts @@ -5,9 +5,8 @@ */ import { connect } from 'react-redux'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { fetchAllRenderables } from '../../../state/actions/elements'; -// @ts-ignore untyped local import { getInFlight } from '../../../state/selectors/resolved_args'; import { State } from '../../../../types'; import { RefreshControl as Component } from './refresh_control'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts index 64712f0df8d6c..1e1eac2a1dcf3 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts @@ -11,7 +11,6 @@ import { getRenderedWorkpad, getRenderedWorkpadExpressions, } from '../../../../state/selectors/workpad'; -// @ts-ignore Untyped local import { downloadRenderedWorkpad, downloadRuntime, diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts index 8a3438e89e846..45257cd4fe308 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/utils.ts @@ -5,7 +5,6 @@ */ import rison from 'rison-node'; -// @ts-ignore Untyped local. import { IBasePath } from 'kibana/public'; import { fetch } from '../../../../common/lib/fetch'; import { CanvasWorkpad } from '../../../../types'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts index 0765973915f77..ddf1a12775cae 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/index.ts @@ -10,9 +10,9 @@ import { Dispatch } from 'redux'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public/'; import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; import { State, CanvasWorkpadBoundingBox } from '../../../../types'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { fetchAllRenderables } from '../../../state/actions/elements'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; import { setWriteable, diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx index 4aab8280a9f24..eb4b451896b46 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.tsx @@ -6,14 +6,13 @@ import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; -// @ts-ignore no @types definition +// @ts-expect-error no @types definition import { Shortcuts } from 'react-shortcuts'; import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { ComponentStrings } from '../../../i18n'; import { ToolTipShortcut } from '../tool_tip_shortcut/'; -// @ts-ignore untyped local import { RefreshControl } from './refresh_control'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { FullscreenControl } from './fullscreen_control'; import { EditMenu } from './edit_menu'; import { ElementMenu } from './element_menu'; diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx index d5841a1069ea1..e1ed7c7db84a0 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx @@ -5,7 +5,7 @@ */ import React, { CSSProperties, PureComponent } from 'react'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { WORKPAD_CONTAINER_ID } from '../../../apps/workpad/workpad_app'; interface State { diff --git a/x-pack/plugins/canvas/public/components/workpad_shortcuts/workpad_shortcuts.tsx b/x-pack/plugins/canvas/public/components/workpad_shortcuts/workpad_shortcuts.tsx index f9e0ec8a8a541..1bb3ef330f846 100644 --- a/x-pack/plugins/canvas/public/components/workpad_shortcuts/workpad_shortcuts.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_shortcuts/workpad_shortcuts.tsx @@ -7,7 +7,7 @@ import React, { Component, KeyboardEvent } from 'react'; import isEqual from 'react-fast-compare'; -// @ts-ignore no @types definition +// @ts-expect-error no @types definition import { Shortcuts } from 'react-shortcuts'; import { isTextInput } from '../../lib/is_text_input'; diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx index 5fdc88ed62406..863cdd88163c2 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx +++ b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx @@ -7,7 +7,7 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import React from 'react'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { getDefaultWorkpad } from '../../../../state/defaults'; import { Arguments, ArgumentTypes, BorderStyle, ExtendedTemplate } from '../extended_template'; diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx index 4ef17fbe87616..2dbff1b4d916b 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx +++ b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx @@ -7,7 +7,7 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import React from 'react'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { getDefaultWorkpad } from '../../../../state/defaults'; import { Argument, Arguments, SimpleTemplate } from '../simple_template'; diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx index f9b175e84ec8e..fa1b2420d46d2 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx +++ b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx @@ -7,7 +7,7 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; import React from 'react'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { getDefaultWorkpad } from '../../../../state/defaults'; import { SimpleTemplate } from '../simple_template'; diff --git a/x-pack/plugins/canvas/public/functions/asset.ts b/x-pack/plugins/canvas/public/functions/asset.ts index 2f2ad181b264c..ebd3fd2abdcbb 100644 --- a/x-pack/plugins/canvas/public/functions/asset.ts +++ b/x-pack/plugins/canvas/public/functions/asset.ts @@ -5,7 +5,7 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; -// @ts-ignore unconverted local lib +// @ts-expect-error unconverted local lib import { getState } from '../state/store'; import { getAssetById } from '../state/selectors/assets'; import { getFunctionHelp, getFunctionErrors } from '../../i18n'; diff --git a/x-pack/plugins/canvas/public/functions/filters.ts b/x-pack/plugins/canvas/public/functions/filters.ts index 78cd742b44b26..48f4a41c7690a 100644 --- a/x-pack/plugins/canvas/public/functions/filters.ts +++ b/x-pack/plugins/canvas/public/functions/filters.ts @@ -8,7 +8,7 @@ import { fromExpression } from '@kbn/interpreter/common'; import { get } from 'lodash'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; import { interpretAst } from '../lib/run_interpreter'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { getState } from '../state/store'; import { getGlobalFilters } from '../state/selectors/workpad'; import { ExpressionValueFilter } from '../../types'; diff --git a/x-pack/plugins/canvas/public/functions/timelion.ts b/x-pack/plugins/canvas/public/functions/timelion.ts index abb294d9cc110..4eb34e838d18a 100644 --- a/x-pack/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/plugins/canvas/public/functions/timelion.ts @@ -9,7 +9,7 @@ import moment from 'moment-timezone'; import { TimeRange } from 'src/plugins/data/common'; import { ExpressionFunctionDefinition, DatatableRow } from 'src/plugins/expressions/public'; import { fetch } from '../../common/lib/fetch'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { buildBoolArray } from '../../public/lib/build_bool_array'; import { Datatable, ExpressionValueFilter } from '../../types'; import { getFunctionHelp } from '../../i18n'; diff --git a/x-pack/plugins/canvas/public/functions/to.ts b/x-pack/plugins/canvas/public/functions/to.ts index 64d25b28a8aa0..032873dfa6cf2 100644 --- a/x-pack/plugins/canvas/public/functions/to.ts +++ b/x-pack/plugins/canvas/public/functions/to.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore untyped Elastic library +// @ts-expect-error untyped Elastic library import { castProvider } from '@kbn/interpreter/common'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/public'; import { getFunctionHelp, getFunctionErrors } from '../../i18n'; diff --git a/x-pack/plugins/canvas/public/lib/app_state.ts b/x-pack/plugins/canvas/public/lib/app_state.ts index d431202ba75a4..a09df3c8cb87d 100644 --- a/x-pack/plugins/canvas/public/lib/app_state.ts +++ b/x-pack/plugins/canvas/public/lib/app_state.ts @@ -6,12 +6,12 @@ import { parse } from 'query-string'; import { get } from 'lodash'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { getInitialState } from '../state/initial_state'; import { getWindow } from './get_window'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { historyProvider } from './history_provider'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { routerProvider } from './router_provider'; import { createTimeInterval, isValidTimeInterval, getTimeInterval } from './time_interval'; import { AppState, AppStateKeys } from '../../types'; diff --git a/x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts b/x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts index c847bfb6516bf..94d0d16bf79f6 100644 --- a/x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts +++ b/x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts @@ -5,7 +5,7 @@ */ import { ExpressionValueFilter } from '../../types'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { buildBoolArray } from './build_bool_array'; import { TimeRange, esFilters, Filter as DataFilter } from '../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/canvas/public/lib/clipboard.test.ts b/x-pack/plugins/canvas/public/lib/clipboard.test.ts index d10964003ed39..53f92e2184edc 100644 --- a/x-pack/plugins/canvas/public/lib/clipboard.test.ts +++ b/x-pack/plugins/canvas/public/lib/clipboard.test.ts @@ -15,7 +15,7 @@ const get = jest.fn(); describe('clipboard', () => { beforeAll(() => { - // @ts-ignore + // @ts-expect-error Storage.mockImplementation(() => ({ set, get, diff --git a/x-pack/plugins/canvas/public/lib/clone_subgraphs.ts b/x-pack/plugins/canvas/public/lib/clone_subgraphs.ts index c3a3933e06a6d..7168272211d44 100644 --- a/x-pack/plugins/canvas/public/lib/clone_subgraphs.ts +++ b/x-pack/plugins/canvas/public/lib/clone_subgraphs.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { arrayToMap } from './aeroelastic/functional'; import { getId } from './get_id'; import { PositionedElement } from '../../types'; diff --git a/x-pack/plugins/canvas/public/lib/create_thunk.ts b/x-pack/plugins/canvas/public/lib/create_thunk.ts index cbcaeeccc8b93..8ce912246ad6f 100644 --- a/x-pack/plugins/canvas/public/lib/create_thunk.ts +++ b/x-pack/plugins/canvas/public/lib/create_thunk.ts @@ -5,7 +5,7 @@ */ import { Dispatch, Action } from 'redux'; -// @ts-ignore untyped dependency +// @ts-expect-error untyped dependency import { createThunk as createThunkFn } from 'redux-thunks/cjs'; import { State } from '../../types'; diff --git a/x-pack/plugins/canvas/public/lib/download_workpad.ts b/x-pack/plugins/canvas/public/lib/download_workpad.ts index fb038d8b6ace2..d0a63cf3fb5c4 100644 --- a/x-pack/plugins/canvas/public/lib/download_workpad.ts +++ b/x-pack/plugins/canvas/public/lib/download_workpad.ts @@ -7,7 +7,7 @@ import fileSaver from 'file-saver'; import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../common/lib/constants'; import { ErrorStrings } from '../../i18n'; import { notifyService } from '../services'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import * as workpadService from './workpad_service'; import { CanvasRenderedWorkpad } from '../../shareable_runtime/types'; diff --git a/x-pack/plugins/canvas/public/lib/element_handler_creators.ts b/x-pack/plugins/canvas/public/lib/element_handler_creators.ts index a2bf5a62ec1f7..8f1a0f0ecf08f 100644 --- a/x-pack/plugins/canvas/public/lib/element_handler_creators.ts +++ b/x-pack/plugins/canvas/public/lib/element_handler_creators.ts @@ -5,9 +5,7 @@ */ import { camelCase } from 'lodash'; -// @ts-ignore unconverted local file import { getClipboardData, setClipboardData } from './clipboard'; -// @ts-ignore unconverted local file import { cloneSubgraphs } from './clone_subgraphs'; import { notifyService } from '../services'; import * as customElementService from './custom_element_service'; diff --git a/x-pack/plugins/canvas/public/lib/es_service.ts b/x-pack/plugins/canvas/public/lib/es_service.ts index 496751a874b21..5c1131d5fbe35 100644 --- a/x-pack/plugins/canvas/public/lib/es_service.ts +++ b/x-pack/plugins/canvas/public/lib/es_service.ts @@ -7,7 +7,6 @@ import { IndexPatternAttributes } from 'src/plugins/data/public'; import { API_ROUTE } from '../../common/lib/constants'; -// @ts-ignore untyped local import { fetch } from '../../common/lib/fetch'; import { ErrorStrings } from '../../i18n'; import { notifyService } from '../services'; diff --git a/x-pack/plugins/canvas/public/lib/sync_filter_expression.ts b/x-pack/plugins/canvas/public/lib/sync_filter_expression.ts index dc70f778f0e52..4bfe6ff4b141f 100644 --- a/x-pack/plugins/canvas/public/lib/sync_filter_expression.ts +++ b/x-pack/plugins/canvas/public/lib/sync_filter_expression.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore internal untyped import { fromExpression } from '@kbn/interpreter/common'; import immutable from 'object-path-immutable'; import { get } from 'lodash'; diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 9d2a6b3fdf4f4..4829a94bb0db8 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -24,7 +24,7 @@ import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { argTypeSpecs } from './expression_types/arg_types'; import { transitions } from './transitions'; import { getPluginApi, CanvasApi } from './plugin_api'; diff --git a/x-pack/plugins/canvas/public/registries.ts b/x-pack/plugins/canvas/public/registries.ts index 99f309a917329..b2881fc0b7799 100644 --- a/x-pack/plugins/canvas/public/registries.ts +++ b/x-pack/plugins/canvas/public/registries.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore untyped module +// @ts-expect-error untyped module import { addRegistries, register } from '@kbn/interpreter/common'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { elementsRegistry } from './lib/elements_registry'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { templatesRegistry } from './lib/templates_registry'; import { tagsRegistry } from './lib/tags_registry'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { transitionsRegistry } from './lib/transitions_registry'; import { @@ -20,7 +20,7 @@ import { modelRegistry, transformRegistry, viewRegistry, - // @ts-ignore untyped local + // @ts-expect-error untyped local } from './expression_types'; import { SetupRegistries } from './plugin_api'; diff --git a/x-pack/plugins/canvas/public/state/actions/embeddable.ts b/x-pack/plugins/canvas/public/state/actions/embeddable.ts index a153cb7f4354d..874d390277320 100644 --- a/x-pack/plugins/canvas/public/state/actions/embeddable.ts +++ b/x-pack/plugins/canvas/public/state/actions/embeddable.ts @@ -7,7 +7,7 @@ import { Dispatch } from 'redux'; import { createAction } from 'redux-actions'; import { createThunk } from '../../lib/create_thunk'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { fetchRenderable } from './elements'; import { State } from '../../../types'; diff --git a/x-pack/plugins/canvas/public/state/actions/workpad.ts b/x-pack/plugins/canvas/public/state/actions/workpad.ts index 47df38838f890..419832e404594 100644 --- a/x-pack/plugins/canvas/public/state/actions/workpad.ts +++ b/x-pack/plugins/canvas/public/state/actions/workpad.ts @@ -8,7 +8,7 @@ import { createAction } from 'redux-actions'; import { without, includes } from 'lodash'; import { createThunk } from '../../lib/create_thunk'; import { getWorkpadColors } from '../selectors/workpad'; -// @ts-ignore +// @ts-expect-error import { fetchAllRenderables } from './elements'; import { CanvasWorkpad } from '../../../types'; diff --git a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_autoplay.test.ts b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_autoplay.test.ts index 11ebdcdc51d4d..bb7b26919ef20 100644 --- a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_autoplay.test.ts +++ b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_autoplay.test.ts @@ -10,7 +10,7 @@ jest.mock('../../../lib/router_provider'); import { workpadAutoplay } from '../workpad_autoplay'; import { setAutoplayInterval } from '../../../lib/app_state'; import { createTimeInterval } from '../../../lib/time_interval'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { routerProvider } from '../../../lib/router_provider'; const next = jest.fn(); diff --git a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts index f90f570bc6ebf..bf69a862d5c30 100644 --- a/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts +++ b/x-pack/plugins/canvas/public/state/middleware/__tests__/workpad_refresh.test.ts @@ -9,7 +9,6 @@ jest.mock('../../../lib/app_state'); import { workpadRefresh } from '../workpad_refresh'; import { inFlightComplete } from '../../actions/resolved_args'; -// @ts-ignore untyped local import { setRefreshInterval } from '../../actions/workpad'; import { setRefreshInterval as setAppStateRefreshInterval } from '../../../lib/app_state'; diff --git a/x-pack/plugins/canvas/public/state/middleware/in_flight.ts b/x-pack/plugins/canvas/public/state/middleware/in_flight.ts index 7ad6f8aee15ed..028b9f214133f 100644 --- a/x-pack/plugins/canvas/public/state/middleware/in_flight.ts +++ b/x-pack/plugins/canvas/public/state/middleware/in_flight.ts @@ -9,7 +9,7 @@ import { loadingIndicator as defaultLoadingIndicator, LoadingIndicatorInterface, } from '../../lib/loading_indicator'; -// @ts-ignore +// @ts-expect-error import { convert } from '../../lib/modify_path'; interface InFlightMiddlewareOptions { diff --git a/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.ts b/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.ts index dd484521c1b35..f77a1e1ba3295 100644 --- a/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.ts +++ b/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.ts @@ -9,9 +9,9 @@ import { State } from '../../../types'; import { getFullscreen } from '../selectors/app'; import { getInFlight } from '../selectors/resolved_args'; import { getWorkpad, getPages, getSelectedPageIndex, getAutoplay } from '../selectors/workpad'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { appUnload } from '../actions/app'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { routerProvider } from '../../lib/router_provider'; import { setAutoplayInterval } from '../../lib/app_state'; import { createTimeInterval } from '../../lib/time_interval'; diff --git a/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.ts b/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.ts index 96a84b22cfccc..4a17ffb464532 100644 --- a/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.ts +++ b/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.ts @@ -6,11 +6,10 @@ import { Middleware } from 'redux'; import { State } from '../../../types'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { fetchAllRenderables } from '../actions/elements'; -// @ts-ignore Untyped Local import { setRefreshInterval } from '../actions/workpad'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { appUnload } from '../actions/app'; import { inFlightComplete } from '../actions/resolved_args'; import { getInFlight } from '../selectors/resolved_args'; diff --git a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts index 8642239fa10d3..fdeb5087f26e1 100644 --- a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts +++ b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts @@ -13,7 +13,7 @@ import { UpdateEmbeddableExpressionPayload, } from '../actions/embeddable'; -// @ts-ignore untyped local +// @ts-expect-error untyped local import { assignNodeProperties } from './elements'; export const embeddableReducer = handleActions< diff --git a/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts b/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts index 9e2036e02f2b9..766e27d95da9b 100644 --- a/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts +++ b/x-pack/plugins/canvas/public/state/selectors/resolved_args.ts @@ -5,9 +5,9 @@ */ import { get } from 'lodash'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import * as argHelper from '../../lib/resolved_arg'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { prepend } from '../../lib/modify_path'; import { State } from '../../../types'; diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/plugins/canvas/public/state/selectors/workpad.ts index 55bf2a7ea31f7..0f4953ff56d98 100644 --- a/x-pack/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/plugins/canvas/public/state/selectors/workpad.ts @@ -5,9 +5,9 @@ */ import { get, omit } from 'lodash'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { safeElementFromExpression, fromExpression } from '@kbn/interpreter/common'; -// @ts-ignore Untyped Local +// @ts-expect-error untyped local import { append } from '../../lib/modify_path'; import { getAssets } from './assets'; import { State, CanvasWorkpad, CanvasPage, CanvasElement, ResolvedArgType } from '../../../types'; diff --git a/x-pack/plugins/canvas/public/store.ts b/x-pack/plugins/canvas/public/store.ts index 81edec6ec539c..ef93a34296da2 100644 --- a/x-pack/plugins/canvas/public/store.ts +++ b/x-pack/plugins/canvas/public/store.ts @@ -9,9 +9,9 @@ import { destroyStore as destroy, getStore, cloneStore, - // @ts-ignore Untyped local + // @ts-expect-error untyped local } from './state/store'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { getInitialState } from './state/initial_state'; import { CoreSetup } from '../../../../src/core/public'; diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts index 8f3ced13895f6..7a9830124e305 100644 --- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts +++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.ts @@ -8,7 +8,7 @@ import { mapValues, keys } from 'lodash'; import { schema } from '@kbn/config-schema'; import { API_ROUTE } from '../../../common/lib'; import { catchErrorHandler } from '../catch_error_handler'; -// @ts-ignore unconverted lib +// @ts-expect-error unconverted lib import { normalizeType } from '../../lib/normalize_type'; import { RouteInitializerDeps } from '..'; diff --git a/x-pack/plugins/canvas/server/routes/shareables/download.ts b/x-pack/plugins/canvas/server/routes/shareables/download.ts index 3f331c1635e16..0c86f8472c791 100644 --- a/x-pack/plugins/canvas/server/routes/shareables/download.ts +++ b/x-pack/plugins/canvas/server/routes/shareables/download.ts @@ -21,7 +21,6 @@ export function initializeDownloadShareableWorkpadRoute(deps: RouteInitializerDe // // The option setting is not for typical use. We're using it here to avoid // problems in Cloud environments. See elastic/kibana#47405. - // @ts-ignore No type for inert Hapi handler // const file = handler.file(SHAREABLE_RUNTIME_FILE, { confine: false }); const file = readFileSync(SHAREABLE_RUNTIME_FILE); return response.ok({ diff --git a/x-pack/plugins/canvas/server/sample_data/load_sample_data.ts b/x-pack/plugins/canvas/server/sample_data/load_sample_data.ts index f58111000859a..f5dcf59dcf45b 100644 --- a/x-pack/plugins/canvas/server/sample_data/load_sample_data.ts +++ b/x-pack/plugins/canvas/server/sample_data/load_sample_data.ts @@ -6,7 +6,6 @@ import { SampleDataRegistrySetup } from 'src/plugins/home/server'; import { CANVAS as label } from '../../i18n'; -// @ts-ignore Untyped local import { ecommerceSavedObjects, flightsSavedObjects, webLogsSavedObjects } from './index'; export function loadSampleData( @@ -16,9 +15,9 @@ export function loadSampleData( const now = new Date(); const nowTimestamp = now.toISOString(); - // @ts-ignore: Untyped local + // @ts-expect-error: untyped local function updateCanvasWorkpadTimestamps(savedObjects) { - // @ts-ignore: Untyped local + // @ts-expect-error: untyped local return savedObjects.map((savedObject) => { if (savedObject.type === 'canvas-workpad') { savedObject.attributes['@timestamp'] = nowTimestamp; diff --git a/x-pack/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx b/x-pack/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx index d99c9b190f83d..4b3aa8dc2fb6e 100644 --- a/x-pack/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/api/__tests__/shareable.test.tsx @@ -15,7 +15,7 @@ jest.mock('../../supported_renderers'); describe('Canvas Shareable Workpad API', () => { // Mock the AJAX load of the workpad. beforeEach(function () { - // @ts-ignore Applying a global in Jest is alright. + // @ts-expect-error Applying a global in Jest is alright. global.fetch = jest.fn().mockImplementation(() => { const p = new Promise((resolve, _reject) => { resolve({ diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx index 7b5a5080ae790..899edee7f0481 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx @@ -7,7 +7,7 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; import { ExampleContext } from '../../test/context_example'; -// @ts-ignore +// @ts-expect-error import { image } from '../../../canvas_plugin_src/renderers/image'; import { sharedWorkpads } from '../../test'; import { RenderedElement, RenderedElementComponent } from '../rendered_element'; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/autoplay_settings.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/autoplay_settings.tsx index 1650cbad3a237..4c7c65511698d 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/autoplay_settings.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/autoplay_settings.tsx @@ -12,7 +12,6 @@ import { setAutoplayIntervalAction, } from '../../../context'; import { createTimeInterval } from '../../../../public/lib/time_interval'; -// @ts-ignore Untyped local import { CustomInterval } from '../../../../public/components/workpad_header/view_menu/custom_interval'; export type onSetAutoplayFn = (autoplay: boolean) => void; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx b/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx index c4a009db3a376..5741f5f2d698c 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx @@ -5,11 +5,11 @@ */ import React, { FC, PureComponent } from 'react'; -// @ts-ignore Untyped library +// @ts-expect-error untyped library import Style from 'style-it'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { Positionable } from '../../public/components/positionable/positionable'; -// @ts-ignore Untyped local +// @ts-expect-error untyped local import { elementToShape } from '../../public/components/workpad_page/utils'; import { CanvasRenderedElement } from '../types'; import { CanvasShareableContext, useCanvasShareableState } from '../context'; diff --git a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js index 6238aaf5c2fe4..340d1fb418b4c 100644 --- a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js +++ b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// This is a JS file because the renderers are not strongly-typed yet. Tagging for -// visibility. -// @ts-ignore Untyped local - import { debug } from '../canvas_plugin_src/renderers/debug'; import { error } from '../canvas_plugin_src/renderers/error'; import { image } from '../canvas_plugin_src/renderers/image'; diff --git a/x-pack/plugins/canvas/shareable_runtime/types.ts b/x-pack/plugins/canvas/shareable_runtime/types.ts index 191c0405d2e2d..040062346c74f 100644 --- a/x-pack/plugins/canvas/shareable_runtime/types.ts +++ b/x-pack/plugins/canvas/shareable_runtime/types.ts @@ -5,7 +5,7 @@ */ import { RefObject } from 'react'; -// @ts-ignore Unlinked Webpack Type +// @ts-expect-error Unlinked Webpack Type import ContainerStyle from 'types/interpreter'; import { SavedObject, SavedObjectAttributes } from 'src/core/public'; From cfe3083379c08cd859f743ee2e20afb8fdde234a Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Wed, 24 Jun 2020 10:10:34 -0600 Subject: [PATCH 04/82] [data.search.aggs]: Add AggConfig.toSerializedFieldFormat (#69114) (#69803) --- ...plugin-plugins-data-public.fieldformats.md | 1 - ...plugin-plugins-data-server.fieldformats.md | 1 - ...plugin-plugins-data-server.plugin.setup.md | 4 +- .../loader/pipeline_helpers/index.ts | 21 ---- .../loader/pipeline_helpers/utilities.ts | 31 ----- .../data/common/field_formats/index.ts | 2 +- .../data/common/field_formats/utils/index.ts | 1 - .../common/field_formats/utils/serialize.ts | 53 --------- src/plugins/data/public/index.ts | 3 - src/plugins/data/public/public.api.md | 72 ++++++------ .../public/search/aggs/agg_config.test.ts | 109 +++++++++++++++++- .../data/public/search/aggs/agg_config.ts | 23 +++- .../data/public/search/aggs/agg_type.test.ts | 68 +++++++++++ .../data/public/search/aggs/agg_type.ts | 23 +++- .../search/aggs/buckets/date_histogram.ts | 8 ++ .../public/search/aggs/buckets/date_range.ts | 6 + .../public/search/aggs/buckets/ip_range.ts | 6 + .../buckets/lib/time_buckets/time_buckets.ts | 2 +- .../public/search/aggs/buckets/range.test.ts | 20 +++- .../data/public/search/aggs/buckets/range.ts | 10 ++ .../data/public/search/aggs/buckets/terms.ts | 12 ++ .../public/search/aggs/metrics/bucket_avg.ts | 9 +- .../public/search/aggs/metrics/bucket_max.ts | 9 +- .../public/search/aggs/metrics/bucket_min.ts | 9 +- .../public/search/aggs/metrics/bucket_sum.ts | 9 +- .../public/search/aggs/metrics/cardinality.ts | 5 + .../data/public/search/aggs/metrics/count.ts | 5 + .../search/aggs/metrics/cumulative_sum.ts | 9 +- .../public/search/aggs/metrics/derivative.ts | 9 +- .../metrics/lib/parent_pipeline_agg_helper.ts | 12 ++ .../lib/sibling_pipeline_agg_helper.ts | 5 + .../public/search/aggs/metrics/moving_avg.ts | 9 +- .../search/aggs/metrics/percentile_ranks.ts | 5 + .../public/search/aggs/metrics/serial_diff.ts | 9 +- .../test_helpers/mock_agg_types_registry.ts | 22 ++++ .../data/public/search/expressions/esaggs.ts | 11 +- src/plugins/data/server/index.ts | 2 - src/plugins/data/server/server.api.md | 51 ++++---- .../public/application/angular/discover.js | 5 +- .../public/legacy/build_pipeline.test.ts | 36 +++--- .../public/legacy/build_pipeline.ts | 17 +-- tsconfig.types.json | 2 + 42 files changed, 472 insertions(+), 254 deletions(-) delete mode 100644 src/legacy/ui/public/visualize/loader/pipeline_helpers/index.ts delete mode 100644 src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts delete mode 100644 src/plugins/data/common/field_formats/utils/serialize.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md index d39871b99f744..b51421741933a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md @@ -10,7 +10,6 @@ fieldFormats: { FieldFormat: typeof FieldFormat; FieldFormatsRegistry: typeof FieldFormatsRegistry; - serialize: (agg: import("./search").AggConfig) => import("../../expressions").SerializedFieldFormat; DEFAULT_CONVERTER_COLOR: { range: string; regex: string; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md index 11f18a195d271..45fc1a608e8ca 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md @@ -10,7 +10,6 @@ fieldFormats: { FieldFormatsRegistry: typeof FieldFormatsRegistry; FieldFormat: typeof FieldFormat; - serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions").SerializedFieldFormat; BoolFormat: typeof BoolFormat; BytesFormat: typeof BytesFormat; ColorFormat: typeof ColorFormat; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md index 13c69d6bf7548..a6fdfdf6891c8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md @@ -10,7 +10,7 @@ setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { search: ISearchSetup; fieldFormats: { - register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; }; ``` @@ -27,7 +27,7 @@ setup(core: CoreSetup, { usageCollection }: DataPluginS `{ search: ISearchSetup; fieldFormats: { - register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; }` diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/index.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/index.ts deleted file mode 100644 index f9a2234d6e5a4..0000000000000 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { buildPipeline } from '../../../../../../plugins/visualizations/public/legacy/build_pipeline'; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts deleted file mode 100644 index cb25c66dfd2fe..0000000000000 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { npStart } from 'ui/new_platform'; -import { fieldFormats, IFieldFormat } from '../../../../../../plugins/data/public'; -import { SerializedFieldFormat } from '../../../../../../plugins/expressions/common/types'; - -type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; - -const createFormat = fieldFormats.serialize; -const getFormat: FormatFactory = (mapping?) => { - return npStart.plugins.data.fieldFormats.deserialize(mapping as any); -}; - -export { getFormat, createFormat, FormatFactory }; diff --git a/src/plugins/data/common/field_formats/index.ts b/src/plugins/data/common/field_formats/index.ts index 5c67073c07dd5..104ff030873aa 100644 --- a/src/plugins/data/common/field_formats/index.ts +++ b/src/plugins/data/common/field_formats/index.ts @@ -40,7 +40,7 @@ export { TruncateFormat, } from './converters'; -export { getHighlightRequest, serializeFieldFormat } from './utils'; +export { getHighlightRequest } from './utils'; export { DEFAULT_CONVERTER_COLOR } from './constants/color_default'; export { FIELD_FORMAT_IDS } from './types'; diff --git a/src/plugins/data/common/field_formats/utils/index.ts b/src/plugins/data/common/field_formats/utils/index.ts index 3832c941ffad7..eb020c17ca09c 100644 --- a/src/plugins/data/common/field_formats/utils/index.ts +++ b/src/plugins/data/common/field_formats/utils/index.ts @@ -22,6 +22,5 @@ import { IFieldFormat } from '../index'; export { asPrettyString } from './as_pretty_string'; export { getHighlightHtml, getHighlightRequest } from './highlight'; -export { serializeFieldFormat } from './serialize'; export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; diff --git a/src/plugins/data/common/field_formats/utils/serialize.ts b/src/plugins/data/common/field_formats/utils/serialize.ts deleted file mode 100644 index 1092c90d19451..0000000000000 --- a/src/plugins/data/common/field_formats/utils/serialize.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IAggConfig } from 'src/plugins/data/public'; -import { SerializedFieldFormat } from '../../../../expressions/common/types'; - -export const serializeFieldFormat = (agg: IAggConfig): SerializedFieldFormat => { - const format: SerializedFieldFormat = agg.params.field ? agg.params.field.format.toJSON() : {}; - const formats: Record SerializedFieldFormat> = { - date_range: () => ({ id: 'date_range', params: format }), - ip_range: () => ({ id: 'ip_range', params: format }), - percentile_ranks: () => ({ id: 'percent' }), - count: () => ({ id: 'number' }), - cardinality: () => ({ id: 'number' }), - date_histogram: () => ({ - id: 'date', - params: { - pattern: (agg as any).buckets.getScaledDateFormat(), - }, - }), - terms: () => ({ - id: 'terms', - params: { - id: format.id, - otherBucketLabel: agg.params.otherBucketLabel, - missingBucketLabel: agg.params.missingBucketLabel, - ...format.params, - }, - }), - range: () => ({ - id: 'range', - params: { id: format.id, ...format.params }, - }), - }; - - return formats[agg.type.name] ? formats[agg.type.name]() : format; -}; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index e57b02784ca9e..d3552cf72a27e 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -168,7 +168,6 @@ import { UrlFormat, StringFormat, TruncateFormat, - serializeFieldFormat, } from '../common/field_formats'; import { DateFormat } from './field_formats'; @@ -179,8 +178,6 @@ export const fieldFormats = { FieldFormat, FieldFormatsRegistry, // exported only for tests. Consider mock. - serialize: serializeFieldFormat, - DEFAULT_CONVERTER_COLOR, HTML_CONTEXT_TYPE, TEXT_CONTEXT_TYPE, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index a219b6660a9e7..6d6bb09ccd7dc 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -154,6 +154,7 @@ import { SearchParams } from 'elasticsearch'; import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; import { SearchShardsParams } from 'elasticsearch'; import { SearchTemplateParams } from 'elasticsearch'; +import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/public'; import { SimpleSavedObject } from 'src/core/public'; import { SnapshotCreateParams } from 'elasticsearch'; import { SnapshotCreateRepositoryParams } from 'elasticsearch'; @@ -621,7 +622,6 @@ export type FieldFormatInstanceType = (new (params?: any, getConfig?: FieldForma export const fieldFormats: { FieldFormat: typeof FieldFormat; FieldFormatsRegistry: typeof FieldFormatsRegistry; - serialize: (agg: import("./search").AggConfig) => import("../../expressions").SerializedFieldFormat; DEFAULT_CONVERTER_COLOR: { range: string; regex: string; @@ -1964,41 +1964,41 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:136:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:376:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:376:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:376:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:376:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:378:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:379:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:373:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:373:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:373:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:373:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:375:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:376:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:40:60 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:53:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/aggs/agg_config.test.ts b/src/plugins/data/public/search/aggs/agg_config.test.ts index 6a0dad07b69bb..95e0b2cd27186 100644 --- a/src/plugins/data/public/search/aggs/agg_config.test.ts +++ b/src/plugins/data/public/search/aggs/agg_config.test.ts @@ -25,7 +25,11 @@ import { AggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; import { MetricAggType } from './metrics/metric_agg_type'; -import { Field as IndexPatternField, IndexPattern } from '../../index_patterns'; +import { + Field as IndexPatternField, + IndexPattern, + IIndexPatternFieldList, +} from '../../index_patterns'; import { stubIndexPatternWithFields } from '../../../public/stubs'; import { FieldFormatsStart } from '../../field_formats'; import { fieldFormatsServiceMock } from '../../field_formats/mocks'; @@ -370,6 +374,109 @@ describe('AggConfig', () => { }); }); + describe('#toSerializedFieldFormat', () => { + beforeEach(() => { + indexPattern.fields.getByName = identity as IIndexPatternFieldList['getByName']; + }); + + it('works with aggs that have a special format type', () => { + const configStates = [ + { + type: 'count', + params: {}, + }, + { + type: 'date_histogram', + params: { field: '@timestamp' }, + }, + { + type: 'terms', + params: { field: 'machine.os.keyword' }, + }, + ]; + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); + + expect(ac.aggs.map((agg) => agg.toSerializedFieldFormat())).toMatchInlineSnapshot(` + Array [ + Object { + "id": "number", + }, + Object { + "id": "date", + "params": Object { + "pattern": "HH:mm:ss.SSS", + }, + }, + Object { + "id": "terms", + "params": Object { + "id": undefined, + "missingBucketLabel": "Missing", + "otherBucketLabel": "Other", + }, + }, + ] + `); + }); + + it('works with pipeline aggs', () => { + const configStates = [ + { + type: 'max_bucket', + params: { + customMetric: { + type: 'cardinality', + params: { + field: 'bytes', + }, + }, + }, + }, + { + type: 'cumulative_sum', + params: { + buckets_path: '1', + customMetric: { + type: 'cardinality', + params: { + field: 'bytes', + }, + }, + }, + }, + { + type: 'percentile_ranks', + id: 'myMetricAgg', + params: {}, + }, + { + type: 'cumulative_sum', + params: { + metricAgg: 'myMetricAgg', + }, + }, + ]; + const ac = new AggConfigs(indexPattern, configStates, { typesRegistry, fieldFormats }); + + expect(ac.aggs.map((agg) => agg.toSerializedFieldFormat())).toMatchInlineSnapshot(` + Array [ + Object { + "id": "number", + }, + Object { + "id": "number", + }, + Object { + "id": "percent", + }, + Object { + "id": "percent", + }, + ] + `); + }); + }); + describe('#toExpressionAst', () => { beforeEach(() => { fieldFormats.getDefaultInstance = (() => ({ diff --git a/src/plugins/data/public/search/aggs/agg_config.ts b/src/plugins/data/public/search/aggs/agg_config.ts index ee4116eefc0e2..a2b74eca58476 100644 --- a/src/plugins/data/public/search/aggs/agg_config.ts +++ b/src/plugins/data/public/search/aggs/agg_config.ts @@ -20,7 +20,11 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { Assign, Ensure } from '@kbn/utility-types'; -import { ExpressionAstFunction, ExpressionAstArgument } from 'src/plugins/expressions/public'; +import { + ExpressionAstFunction, + ExpressionAstArgument, + SerializedFieldFormat, +} from 'src/plugins/expressions/public'; import { IAggType } from './agg_type'; import { writeParams } from './agg_params'; import { IAggConfigs } from './agg_configs'; @@ -42,7 +46,7 @@ export type AggConfigSerialized = Ensure< type: string; enabled?: boolean; id?: string; - params?: SerializableState; + params?: {} | SerializableState; schema?: string; }, SerializableState @@ -298,8 +302,8 @@ export class AggConfig { id: this.id, enabled: this.enabled, type: this.type && this.type.name, - schema: this.schema, params: outParams as SerializableState, + ...(this.schema && { schema: this.schema }), }; } @@ -310,6 +314,19 @@ export class AggConfig { return this.serialize(); } + /** + * Returns a serialized field format for the field used in this agg. + * This can be passed to fieldFormats.deserialize to get the field + * format instance. + * + * @public + */ + toSerializedFieldFormat(): + | {} + | Ensure, SerializableState> { + return this.type ? this.type.getSerializedFormat(this) : {}; + } + /** * @returns Returns an ExpressionAst representing the function for this agg type. */ diff --git a/src/plugins/data/public/search/aggs/agg_type.test.ts b/src/plugins/data/public/search/aggs/agg_type.test.ts index 18783bbd9a760..cc45b935d45b5 100644 --- a/src/plugins/data/public/search/aggs/agg_type.test.ts +++ b/src/plugins/data/public/search/aggs/agg_type.test.ts @@ -199,5 +199,73 @@ describe('AggType Class', () => { expect(aggType.getFormat(aggConfig)).toBe('default'); }); }); + + describe('getSerializedFormat', () => { + test('returns the default serialized field format if it exists', () => { + const aggConfig = ({ + params: { + field: { + format: { + toJSON: () => ({ id: 'format' }), + }, + }, + }, + } as unknown) as IAggConfig; + const aggType = new AggType( + { + name: 'name', + title: 'title', + }, + dependencies + ); + expect(aggType.getSerializedFormat(aggConfig)).toMatchInlineSnapshot(` + Object { + "id": "format", + } + `); + }); + + test('returns an empty object if a field param does not exist', () => { + const aggConfig = ({ + params: {}, + } as unknown) as IAggConfig; + const aggType = new AggType( + { + name: 'name', + title: 'title', + }, + dependencies + ); + expect(aggType.getSerializedFormat(aggConfig)).toMatchInlineSnapshot(`Object {}`); + }); + + test('uses a custom getSerializedFormat function if defined', () => { + const aggConfig = ({ + params: { + field: { + format: { + toJSON: () => ({ id: 'format' }), + }, + }, + }, + } as unknown) as IAggConfig; + const getSerializedFormat = jest.fn().mockReturnValue({ id: 'hello' }); + const aggType = new AggType( + { + name: 'name', + title: 'title', + getSerializedFormat, + }, + dependencies + ); + const serialized = aggType.getSerializedFormat(aggConfig); + expect(getSerializedFormat).toHaveBeenCalledWith(aggConfig); + expect(serialized).toMatchInlineSnapshot(` + Object { + "id": "hello", + } + `); + }); + }); }); }); diff --git a/src/plugins/data/public/search/aggs/agg_type.ts b/src/plugins/data/public/search/aggs/agg_type.ts index fb0cb609a08cf..e909cd8134e83 100644 --- a/src/plugins/data/public/search/aggs/agg_type.ts +++ b/src/plugins/data/public/search/aggs/agg_type.ts @@ -19,8 +19,10 @@ import { constant, noop, identity } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { initParams } from './agg_params'; +import { SerializedFieldFormat } from 'src/plugins/expressions/public'; + +import { initParams } from './agg_params'; import { AggConfig } from './agg_config'; import { IAggConfigs } from './agg_configs'; import { Adapters } from '../../../../../plugins/inspector/public'; @@ -57,6 +59,7 @@ export interface AggTypeConfig< abortSignal?: AbortSignal ) => Promise; getFormat?: (agg: TAggConfig) => IFieldFormat; + getSerializedFormat?: (agg: TAggConfig) => SerializedFieldFormat; getValue?: (agg: TAggConfig, bucket: any) => any; getKey?: (bucket: any, key: any, agg: TAggConfig) => any; } @@ -204,6 +207,17 @@ export class AggType< */ getFormat: (agg: TAggConfig) => IFieldFormat; + /** + * Get the serialized format for the values produced by this agg type, + * overridden by several metrics that always output a simple number. + * You can pass this output to fieldFormatters.deserialize to get + * the formatter instance. + * + * @param {agg} agg - the agg to pick a format for + * @return {SerializedFieldFormat} + */ + getSerializedFormat: (agg: TAggConfig) => SerializedFieldFormat; + getValue: (agg: TAggConfig, bucket: any) => any; getKey?: (bucket: any, key: any, agg: TAggConfig) => any; @@ -277,6 +291,13 @@ export class AggType< return field ? field.format : fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING); }); + + this.getSerializedFormat = + config.getSerializedFormat || + ((agg: TAggConfig) => { + return agg.params.field ? agg.params.field.format.toJSON() : {}; + }); + this.getValue = config.getValue || ((agg: TAggConfig, bucket: any) => {}); } } diff --git a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts index 8a5596f669cb7..fc42d43b2fea8 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_histogram.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_histogram.ts @@ -152,6 +152,14 @@ export const getDateHistogramBucketAgg = ({ (key: string) => uiSettings.get(key) ); }, + getSerializedFormat(agg) { + return { + id: 'date', + params: { + pattern: agg.buckets.getScaledDateFormat(), + }, + }; + }, params: [ { name: 'field', diff --git a/src/plugins/data/public/search/aggs/buckets/date_range.ts b/src/plugins/data/public/search/aggs/buckets/date_range.ts index 447347dbfbe10..3e14ab422ccbe 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_range.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_range.ts @@ -70,6 +70,12 @@ export const getDateRangeBucketAgg = ({ }); return new DateRangeFormat(); }, + getSerializedFormat(agg) { + return { + id: 'date_range', + params: agg.params.field ? agg.params.field.format.toJSON() : {}, + }; + }, makeLabel(aggConfig) { return aggConfig.getFieldDisplayName() + ' date ranges'; }, diff --git a/src/plugins/data/public/search/aggs/buckets/ip_range.ts b/src/plugins/data/public/search/aggs/buckets/ip_range.ts index 10fdb2d93b56e..b3e90bdf9b56a 100644 --- a/src/plugins/data/public/search/aggs/buckets/ip_range.ts +++ b/src/plugins/data/public/search/aggs/buckets/ip_range.ts @@ -78,6 +78,12 @@ export const getIpRangeBucketAgg = ({ getInternalStartServices }: IpRangeBucketA }); return new IpRangeFormat(); }, + getSerializedFormat(agg) { + return { + id: 'ip_range', + params: agg.params.field ? agg.params.field.format.toJSON() : {}, + }; + }, makeLabel(aggConfig) { return i18n.translate('data.search.aggs.buckets.ipRangeLabel', { defaultMessage: '{fieldName} IP ranges', diff --git a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts index b8d6586652d6b..12197c85f4a96 100644 --- a/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts +++ b/src/plugins/data/public/search/aggs/buckets/lib/time_buckets/time_buckets.ts @@ -48,7 +48,7 @@ function isValidMoment(m: any): boolean { return m && 'isValid' in m && m.isValid(); } -export interface TimeBucketsConfig { +export interface TimeBucketsConfig extends Record { 'histogram:maxBars': number; 'histogram:barTarget': number; dateFormat: string; diff --git a/src/plugins/data/public/search/aggs/buckets/range.test.ts b/src/plugins/data/public/search/aggs/buckets/range.test.ts index 4c2d3af1ab734..2b8a36f2fbdbc 100644 --- a/src/plugins/data/public/search/aggs/buckets/range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/range.test.ts @@ -104,7 +104,7 @@ describe('Range Agg', () => { ); }; - describe('formating', () => { + describe('formatting', () => { test('formats bucket keys properly', () => { const aggConfigs = getAggConfigs(); const agg = aggConfigs.aggs[0]; @@ -115,4 +115,22 @@ describe('Range Agg', () => { expect(format(buckets[2])).toBe('≥ 2.5 KB and < +∞'); }); }); + + describe('getSerializedFormat', () => { + test('generates a serialized field format in the expected shape', () => { + const aggConfigs = getAggConfigs(); + const agg = aggConfigs.aggs[0]; + expect(agg.type.getSerializedFormat(agg)).toMatchInlineSnapshot(` + Object { + "id": "range", + "params": Object { + "id": "number", + "params": Object { + "pattern": "0,0.[000] b", + }, + }, + } + `); + }); + }); }); diff --git a/src/plugins/data/public/search/aggs/buckets/range.ts b/src/plugins/data/public/search/aggs/buckets/range.ts index 02aad3bd5fed1..543e5d66b9fa8 100644 --- a/src/plugins/data/public/search/aggs/buckets/range.ts +++ b/src/plugins/data/public/search/aggs/buckets/range.ts @@ -101,6 +101,16 @@ export const getRangeBucketAgg = ({ getInternalStartServices }: RangeBucketAggDe formats.set(agg, aggFormat); return aggFormat; }, + getSerializedFormat(agg) { + const format = agg.params.field ? agg.params.field.format.toJSON() : {}; + return { + id: 'range', + params: { + id: format.id, + params: format.params, + }, + }; + }, params: [ { name: 'field', diff --git a/src/plugins/data/public/search/aggs/buckets/terms.ts b/src/plugins/data/public/search/aggs/buckets/terms.ts index 45a76f08ddd13..1e8e9ab4ef9d0 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.ts @@ -104,6 +104,18 @@ export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDe }, } as IFieldFormat; }, + getSerializedFormat(agg) { + const format = agg.params.field ? agg.params.field.format.toJSON() : {}; + return { + id: 'terms', + params: { + id: format.id, + otherBucketLabel: agg.params.otherBucketLabel, + missingBucketLabel: agg.params.missingBucketLabel, + ...format.params, + }, + }; + }, createFilter: createFilterTerms, postFlightRequest: async ( resp: any, diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts b/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts index 927e9a7ae4458..38312ec5cfa81 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_avg.ts @@ -46,14 +46,17 @@ const averageBucketTitle = i18n.translate('data.search.aggs.metrics.averageBucke export const getBucketAvgMetricAgg = ({ getInternalStartServices, }: BucketAvgMetricAggDependencies) => { + const { subtype, params, getFormat, getSerializedFormat } = siblingPipelineAggHelper; + return new MetricAggType( { name: METRIC_TYPES.AVG_BUCKET, title: averageBucketTitle, makeLabel: (agg) => makeNestedLabel(agg, overallAverageLabel), - subtype: siblingPipelineAggHelper.subtype, - params: [...siblingPipelineAggHelper.params()], - getFormat: siblingPipelineAggHelper.getFormat, + subtype, + params: [...params()], + getFormat, + getSerializedFormat, getValue(agg, bucket) { const customMetric = agg.getParam('customMetric'); const customBucket = agg.getParam('customBucket'); diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_max.ts b/src/plugins/data/public/search/aggs/metrics/bucket_max.ts index 2b171fcbd24fd..e2c6a5105bac6 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_max.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_max.ts @@ -45,14 +45,17 @@ const maxBucketTitle = i18n.translate('data.search.aggs.metrics.maxBucketTitle', export const getBucketMaxMetricAgg = ({ getInternalStartServices, }: BucketMaxMetricAggDependencies) => { + const { subtype, params, getFormat, getSerializedFormat } = siblingPipelineAggHelper; + return new MetricAggType( { name: METRIC_TYPES.MAX_BUCKET, title: maxBucketTitle, makeLabel: (agg) => makeNestedLabel(agg, overallMaxLabel), - subtype: siblingPipelineAggHelper.subtype, - params: [...siblingPipelineAggHelper.params()], - getFormat: siblingPipelineAggHelper.getFormat, + subtype, + params: [...params()], + getFormat, + getSerializedFormat, }, { getInternalStartServices, diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_min.ts b/src/plugins/data/public/search/aggs/metrics/bucket_min.ts index e6a523eeea374..c46a3eb9425d1 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_min.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_min.ts @@ -45,14 +45,17 @@ const minBucketTitle = i18n.translate('data.search.aggs.metrics.minBucketTitle', export const getBucketMinMetricAgg = ({ getInternalStartServices, }: BucketMinMetricAggDependencies) => { + const { subtype, params, getFormat, getSerializedFormat } = siblingPipelineAggHelper; + return new MetricAggType( { name: METRIC_TYPES.MIN_BUCKET, title: minBucketTitle, makeLabel: (agg) => makeNestedLabel(agg, overallMinLabel), - subtype: siblingPipelineAggHelper.subtype, - params: [...siblingPipelineAggHelper.params()], - getFormat: siblingPipelineAggHelper.getFormat, + subtype, + params: [...params()], + getFormat, + getSerializedFormat, }, { getInternalStartServices, diff --git a/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts b/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts index 71c88596ea569..57212ec9ff91b 100644 --- a/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts +++ b/src/plugins/data/public/search/aggs/metrics/bucket_sum.ts @@ -45,14 +45,17 @@ const sumBucketTitle = i18n.translate('data.search.aggs.metrics.sumBucketTitle', export const getBucketSumMetricAgg = ({ getInternalStartServices, }: BucketSumMetricAggDependencies) => { + const { subtype, params, getFormat, getSerializedFormat } = siblingPipelineAggHelper; + return new MetricAggType( { name: METRIC_TYPES.SUM_BUCKET, title: sumBucketTitle, makeLabel: (agg) => makeNestedLabel(agg, overallSumLabel), - subtype: siblingPipelineAggHelper.subtype, - params: [...siblingPipelineAggHelper.params()], - getFormat: siblingPipelineAggHelper.getFormat, + subtype, + params: [...params()], + getFormat, + getSerializedFormat, }, { getInternalStartServices, diff --git a/src/plugins/data/public/search/aggs/metrics/cardinality.ts b/src/plugins/data/public/search/aggs/metrics/cardinality.ts index 9ff3e84c38cd8..2855cc1b6b16e 100644 --- a/src/plugins/data/public/search/aggs/metrics/cardinality.ts +++ b/src/plugins/data/public/search/aggs/metrics/cardinality.ts @@ -54,6 +54,11 @@ export const getCardinalityMetricAgg = ({ return fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER); }, + getSerializedFormat(agg) { + return { + id: 'number', + }; + }, params: [ { name: 'field', diff --git a/src/plugins/data/public/search/aggs/metrics/count.ts b/src/plugins/data/public/search/aggs/metrics/count.ts index bd0b83798c7db..4c7b8139b0162 100644 --- a/src/plugins/data/public/search/aggs/metrics/count.ts +++ b/src/plugins/data/public/search/aggs/metrics/count.ts @@ -45,6 +45,11 @@ export const getCountMetricAgg = ({ getInternalStartServices }: CountMetricAggDe return fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER); }, + getSerializedFormat(agg) { + return { + id: 'number', + }; + }, getValue(agg, bucket) { return bucket.doc_count; }, diff --git a/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts b/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts index 44bfca1b6fb87..c392f44a7961e 100644 --- a/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts +++ b/src/plugins/data/public/search/aggs/metrics/cumulative_sum.ts @@ -46,14 +46,17 @@ const cumulativeSumTitle = i18n.translate('data.search.aggs.metrics.cumulativeSu export const getCumulativeSumMetricAgg = ({ getInternalStartServices, }: CumulativeSumMetricAggDependencies) => { + const { subtype, params, getFormat, getSerializedFormat } = parentPipelineAggHelper; + return new MetricAggType( { name: METRIC_TYPES.CUMULATIVE_SUM, title: cumulativeSumTitle, - subtype: parentPipelineAggHelper.subtype, makeLabel: (agg) => makeNestedLabel(agg, cumulativeSumLabel), - params: [...parentPipelineAggHelper.params()], - getFormat: parentPipelineAggHelper.getFormat, + subtype, + params: [...params()], + getFormat, + getSerializedFormat, }, { getInternalStartServices, diff --git a/src/plugins/data/public/search/aggs/metrics/derivative.ts b/src/plugins/data/public/search/aggs/metrics/derivative.ts index edb907ca4ed41..f3c1cc9bc2977 100644 --- a/src/plugins/data/public/search/aggs/metrics/derivative.ts +++ b/src/plugins/data/public/search/aggs/metrics/derivative.ts @@ -46,16 +46,19 @@ const derivativeTitle = i18n.translate('data.search.aggs.metrics.derivativeTitle export const getDerivativeMetricAgg = ({ getInternalStartServices, }: DerivativeMetricAggDependencies) => { + const { subtype, params, getFormat, getSerializedFormat } = parentPipelineAggHelper; + return new MetricAggType( { name: METRIC_TYPES.DERIVATIVE, title: derivativeTitle, - subtype: parentPipelineAggHelper.subtype, makeLabel(agg) { return makeNestedLabel(agg, derivativeLabel); }, - params: [...parentPipelineAggHelper.params()], - getFormat: parentPipelineAggHelper.getFormat, + subtype, + params: [...params()], + getFormat, + getSerializedFormat, }, { getInternalStartServices, diff --git a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 947394c97bdcd..2a74a446ce84e 100644 --- a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -84,4 +84,16 @@ export const parentPipelineAggHelper = { } return subAgg ? subAgg.type.getFormat(subAgg) : new (FieldFormat.from(identity))(); }, + + getSerializedFormat(agg: IMetricAggConfig) { + let subAgg; + const customMetric = agg.getParam('customMetric'); + + if (customMetric) { + subAgg = customMetric; + } else { + subAgg = agg.aggConfigs.byId(agg.getParam('metricAgg')); + } + return subAgg ? subAgg.type.getSerializedFormat(subAgg) : {}; + }, }; diff --git a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index cee7841a8c3b9..8e3e0143bf915 100644 --- a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -93,4 +93,9 @@ export const siblingPipelineAggHelper = { ? customMetric.type.getFormat(customMetric) : new (FieldFormat.from(identity))(); }, + + getSerializedFormat(agg: IMetricAggConfig) { + const customMetric = agg.getParam('customMetric'); + return customMetric ? customMetric.type.getSerializedFormat(customMetric) : {}; + }, }; diff --git a/src/plugins/data/public/search/aggs/metrics/moving_avg.ts b/src/plugins/data/public/search/aggs/metrics/moving_avg.ts index 1173ae5358ee7..abad2782f9d20 100644 --- a/src/plugins/data/public/search/aggs/metrics/moving_avg.ts +++ b/src/plugins/data/public/search/aggs/metrics/moving_avg.ts @@ -48,15 +48,19 @@ const movingAvgLabel = i18n.translate('data.search.aggs.metrics.movingAvgLabel', export const getMovingAvgMetricAgg = ({ getInternalStartServices, }: MovingAvgMetricAggDependencies) => { + const { subtype, params, getFormat, getSerializedFormat } = parentPipelineAggHelper; + return new MetricAggType( { name: METRIC_TYPES.MOVING_FN, dslName: 'moving_fn', title: movingAvgTitle, - subtype: parentPipelineAggHelper.subtype, makeLabel: (agg) => makeNestedLabel(agg, movingAvgLabel), + subtype, + getFormat, + getSerializedFormat, params: [ - ...parentPipelineAggHelper.params(), + ...params(), { name: 'window', default: 5, @@ -78,7 +82,6 @@ export const getMovingAvgMetricAgg = ({ */ return bucket[agg.id] ? bucket[agg.id].value : null; }, - getFormat: parentPipelineAggHelper.getFormat, }, { getInternalStartServices, diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts index c8383f6bcc3d9..c5aee380b9776 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.ts @@ -103,6 +103,11 @@ export const getPercentileRanksMetricAgg = ({ fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.NUMBER) ); }, + getSerializedFormat(agg) { + return { + id: 'percent', + }; + }, getValue(agg, bucket) { return getPercentileValue(agg, bucket) / 100; }, diff --git a/src/plugins/data/public/search/aggs/metrics/serial_diff.ts b/src/plugins/data/public/search/aggs/metrics/serial_diff.ts index 00bc631cefab8..7cb1b194fed1c 100644 --- a/src/plugins/data/public/search/aggs/metrics/serial_diff.ts +++ b/src/plugins/data/public/search/aggs/metrics/serial_diff.ts @@ -46,14 +46,17 @@ const serialDiffLabel = i18n.translate('data.search.aggs.metrics.serialDiffLabel export const getSerialDiffMetricAgg = ({ getInternalStartServices, }: SerialDiffMetricAggDependencies) => { + const { subtype, params, getFormat, getSerializedFormat } = parentPipelineAggHelper; + return new MetricAggType( { name: METRIC_TYPES.SERIAL_DIFF, title: serialDiffTitle, - subtype: parentPipelineAggHelper.subtype, makeLabel: (agg) => makeNestedLabel(agg, serialDiffLabel), - params: [...parentPipelineAggHelper.params()], - getFormat: parentPipelineAggHelper.getFormat, + subtype, + params: [...params()], + getFormat, + getSerializedFormat, }, { getInternalStartServices, diff --git a/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts b/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts index 5549ffd2583b1..836aaad2cda0c 100644 --- a/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts +++ b/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts @@ -25,6 +25,25 @@ import { MetricAggType } from '../metrics/metric_agg_type'; import { queryServiceMock } from '../../../query/mocks'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { InternalStartServices } from '../../../types'; +import { TimeBucketsConfig } from '../buckets/lib/time_buckets/time_buckets'; + +// Mocked uiSettings shared among aggs unit tests +const mockUiSettings = jest.fn().mockImplementation((key: string) => { + const config: TimeBucketsConfig = { + 'histogram:maxBars': 4, + 'histogram:barTarget': 3, + dateFormat: 'YYYY-MM-DD', + 'dateFormat:scaled': [ + ['', 'HH:mm:ss.SSS'], + ['PT1S', 'HH:mm:ss'], + ['PT1M', 'HH:mm'], + ['PT1H', 'YYYY-MM-DD HH:mm'], + ['P1DT', 'YYYY-MM-DD'], + ['P1YT', 'YYYY'], + ], + }; + return config[key] ?? key; +}); /** * Testing utility which creates a new instance of AggTypesRegistry, @@ -54,7 +73,10 @@ export function mockAggTypesRegistry | MetricAggTyp }); } else { const coreSetup = coreMock.createSetup(); + coreSetup.uiSettings.get = mockUiSettings; + const coreStart = coreMock.createStart(); + coreSetup.uiSettings.get = mockUiSettings; const aggTypes = getAggTypes({ uiSettings: coreSetup.uiSettings, diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 153eb7de6f2de..77475cd3ce88b 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -32,14 +32,7 @@ import { Adapters } from '../../../../../plugins/inspector/public'; import { IAggConfigs } from '../aggs'; import { ISearchSource } from '../search_source'; import { tabifyAggResponse } from '../tabify'; -import { - Filter, - Query, - serializeFieldFormat, - TimeRange, - IIndexPattern, - isRangeFilter, -} from '../../../common'; +import { Filter, Query, TimeRange, IIndexPattern, isRangeFilter } from '../../../common'; import { FilterManager, calculateBounds, getTime } from '../../query'; import { getSearchService, getQueryService, getIndexPatterns } from '../../services'; import { buildTabularInspectorData } from './build_tabular_inspector_data'; @@ -313,7 +306,7 @@ export const esaggs = (): ExpressionFunctionDefinition import("../../expressions").SerializedFieldFormat; BoolFormat: typeof BoolFormat; BytesFormat: typeof BytesFormat; ColorFormat: typeof ColorFormat; @@ -633,7 +632,7 @@ export class Plugin implements Plugin_2 { setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { search: ISearchSetup; fieldFormats: { - register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; }; // Warning: (ae-forgotten-export) The symbol "CoreStart" needs to be exported by the entry point index.d.ts @@ -776,30 +775,30 @@ export const UI_SETTINGS: { // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "buildEsQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "FieldFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:103:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:131:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:131:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:187:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:189:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:190:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:193:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "FieldFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "ColorFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "DurationFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "IpFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "NumberFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "PercentFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "RelativeDateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "SourceFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "StaticLookupFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:102:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:129:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:129:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:184:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:187:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:188:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:191:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 225f918c9f406..65868b0b7cd46 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -66,7 +66,6 @@ const { import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; import { esFilters, - fieldFormats, indexPatterns as indexPatternsUtils, connectToQueryState, syncQueryStateWithUrl, @@ -851,7 +850,7 @@ function discoverController( x: { accessor: 0, label: agg.makeLabel(), - format: fieldFormats.serialize(agg), + format: agg.toSerializedFieldFormat(), params: { date: true, interval: moment.duration(esValue, esUnit), @@ -863,7 +862,7 @@ function discoverController( }, y: { accessor: 1, - format: fieldFormats.serialize(metric), + format: metric.toSerializedFieldFormat(), label: metric.makeLabel(), }, }; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index 9130581963800..9ecd321963e8a 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -28,7 +28,7 @@ import { } from './build_pipeline'; import { Vis } from '..'; import { dataPluginMock } from '../../../../plugins/data/public/mocks'; -import { IAggConfig } from '../../../../plugins/data/public'; +import { IndexPattern, IAggConfigs } from '../../../../plugins/data/public'; describe('visualize loader pipeline helpers: build pipeline', () => { describe('prepareJson', () => { @@ -344,23 +344,20 @@ describe('visualize loader pipeline helpers: build pipeline', () => { describe('buildVislibDimensions', () => { const dataStart = dataPluginMock.createStartContract(); - let aggs: IAggConfig[]; + let aggs: IAggConfigs; let vis: Vis; let params: any; beforeEach(() => { - aggs = [ + aggs = dataStart.search.aggs.createAggConfigs({} as IndexPattern, [ { id: '0', enabled: true, - type: { - type: 'metrics', - name: 'count', - }, + type: 'count', schema: 'metric', params: {}, - } as IAggConfig, - ]; + }, + ]); params = { searchSource: null, @@ -393,11 +390,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { ], }, data: { - aggs: { - getResponseAggs: () => { - return aggs; - }, - } as any, + aggs, searchSource: {} as any, }, isHierarchical: () => { @@ -422,8 +415,13 @@ describe('visualize loader pipeline helpers: build pipeline', () => { }); it('with two numeric metrics, mixed normal and percent mode should have corresponding formatters', async () => { - const aggConfig = aggs[0]; - aggs = [{ ...aggConfig } as IAggConfig, { ...aggConfig, id: '5' } as IAggConfig]; + aggs.createAggConfig({ + id: '5', + enabled: true, + type: 'count', + schema: 'metric', + params: {}, + }); vis.params = { seriesParams: [ @@ -469,11 +467,7 @@ describe('visualize loader pipeline helpers: build pipeline', () => { }, params: { gauge: {} }, data: { - aggs: { - getResponseAggs: () => { - return aggs; - }, - } as any, + aggs, searchSource: {} as any, }, isHierarchical: () => { diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 5d74cb3d3b1e5..62ff1f83426b9 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -20,12 +20,7 @@ import { get } from 'lodash'; import moment from 'moment'; import { SerializedFieldFormat } from '../../../../plugins/expressions/public'; -import { - IAggConfig, - fieldFormats, - search, - TimefilterContract, -} from '../../../../plugins/data/public'; +import { IAggConfig, search, TimefilterContract } from '../../../../plugins/data/public'; import { Vis, VisParams } from '../types'; const { isDateHistogramBucketAggConfig } = search.aggs; @@ -113,11 +108,9 @@ const getSchemas = ( 'max_bucket', ].includes(agg.type.name); - const format = fieldFormats.serialize( - hasSubAgg - ? agg.params.customMetric || agg.aggConfigs.getRequestAggById(agg.params.metricAgg) - : agg - ); + const formatAgg = hasSubAgg + ? agg.params.customMetric || agg.aggConfigs.getRequestAggById(agg.params.metricAgg) + : agg; const params: SchemaConfigParams = {}; @@ -130,7 +123,7 @@ const getSchemas = ( return { accessor, - format, + format: formatAgg.toSerializedFieldFormat(), params, label, aggType: agg.type.name, diff --git a/tsconfig.types.json b/tsconfig.types.json index fd3624dd8e31b..2f5919e413e51 100644 --- a/tsconfig.types.json +++ b/tsconfig.types.json @@ -10,6 +10,8 @@ "include": [ "src/core/server/index.ts", "src/core/public/index.ts", + "src/plugins/data/server/index.ts", + "src/plugins/data/public/index.ts", "typings" ] } From 59011e9318d4e239ba762e620455c276bf76d8e2 Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Wed, 24 Jun 2020 11:01:43 -0700 Subject: [PATCH 05/82] [Obs] Update Observability landing page text (#69727) (#69770) --- x-pack/plugins/observability/public/pages/home/index.tsx | 4 ++-- x-pack/plugins/observability/public/pages/home/section.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/home/index.tsx b/x-pack/plugins/observability/public/pages/home/index.tsx index 696361393ef82..91e7e2759b824 100644 --- a/x-pack/plugins/observability/public/pages/home/index.tsx +++ b/x-pack/plugins/observability/public/pages/home/index.tsx @@ -92,7 +92,7 @@ export const Home = () => {

{i18n.translate('xpack.observability.home.sectionTitle', { - defaultMessage: 'Observability built on the Elastic Stack', + defaultMessage: 'Unified visibility across your entire ecosystem', })}

@@ -100,7 +100,7 @@ export const Home = () => { {i18n.translate('xpack.observability.home.sectionsubtitle', { defaultMessage: - 'Bring your logs, metrics, and APM traces together at scale in a single stack so you can monitor and react to events happening anywhere in your environment.', + 'Monitor, analyze, and react to events happening anywhere in your environment by bringing logs, metrics, and traces together at scale in a single stack.', })} diff --git a/x-pack/plugins/observability/public/pages/home/section.ts b/x-pack/plugins/observability/public/pages/home/section.ts index a2b82c31bf2ab..d33571a16ccb7 100644 --- a/x-pack/plugins/observability/public/pages/home/section.ts +++ b/x-pack/plugins/observability/public/pages/home/section.ts @@ -23,7 +23,7 @@ export const appsSection: ISection[] = [ icon: 'logoLogging', description: i18n.translate('xpack.observability.section.apps.logs.description', { defaultMessage: - 'The Elastic Stack (sometimes known as the ELK Stack) is the most popular open source logging platform.', + 'Centralize logs from any source. Search, tail, automate anomaly detection, and visualize trends so you can take action quicker.', }), }, { @@ -34,7 +34,7 @@ export const appsSection: ISection[] = [ icon: 'logoAPM', description: i18n.translate('xpack.observability.section.apps.apm.description', { defaultMessage: - 'See exactly where your application is spending time so you can quickly fix issues and feel good about the code you push.', + 'Trace transactions through a distributed architecture and map your services’ interactions to easily spot performance bottlenecks.', }), }, { @@ -45,7 +45,7 @@ export const appsSection: ISection[] = [ icon: 'logoMetrics', description: i18n.translate('xpack.observability.section.apps.metrics.description', { defaultMessage: - 'Already using the Elastic Stack for logs? Add metrics in just a few steps and correlate metrics and logs in one place.', + 'Analyze metrics from your infrastructure, apps, and services. Discover trends, forecast behavior, get alerts on anomalies, and more.', }), }, { @@ -56,7 +56,7 @@ export const appsSection: ISection[] = [ icon: 'logoUptime', description: i18n.translate('xpack.observability.section.apps.uptime.description', { defaultMessage: - 'React to availability issues across your apps and services before they affect users.', + 'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users’ experience.', }), }, ]; From 08648666856e44189fc1d7051729df6a779a284e Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 24 Jun 2020 20:06:48 +0200 Subject: [PATCH 06/82] [Lens] Stabilize filter popover (#69519) (#69821) * stabilize filter popovwer * remove text exclusion --- x-pack/test/functional/apps/lens/smokescreen.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 181d41d77b4cb..b399c9e915e27 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -23,6 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardAddPanel = getService('dashboardAddPanel'); const elasticChart = getService('elasticChart'); const browser = getService('browser'); + const retry = getService('retry'); const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); const listingTable = getService('listingTable'); @@ -93,7 +94,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardAddPanel.closeAddPanel(); await PageObjects.lens.goToTimeRange(); await clickOnBarHistogram(); - await testSubjects.click('applyFiltersPopoverButton'); + + await retry.try(async () => { + await testSubjects.click('applyFiltersPopoverButton'); + await testSubjects.missingOrFail('applyFiltersPopoverButton'); + }); await assertExpectedChart(); await assertExpectedTimerange(); From 0ae2453536ff48e8bcfaa35286d3eb63279bc651 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 24 Jun 2020 14:21:38 -0400 Subject: [PATCH 07/82] [7.x] [IM] Move common step containers to shared (#69713) (#69828) --- .../components/shared/components/index.ts | 7 +++++- .../shared/components/wizard_steps/index.ts | 8 ++++--- .../wizard_steps/step_aliases_container.tsx | 22 ++++++++++++++++++ .../wizard_steps}/step_mappings_container.tsx | 17 ++++++++------ .../wizard_steps/step_settings_container.tsx | 22 ++++++++++++++++++ .../shared/components/wizard_steps/types.ts | 13 +++++++++++ .../application/components/shared/index.ts | 7 +++--- .../components/template_form/steps/index.ts | 3 --- .../steps/step_aliases_container.tsx | 23 ------------------- .../steps/step_settings_container.tsx | 23 ------------------- .../template_form/template_form.tsx | 18 +++++++-------- 11 files changed, 90 insertions(+), 73 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx rename x-pack/plugins/index_management/public/application/components/{template_form/steps => shared/components/wizard_steps}/step_mappings_container.tsx (57%) create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts delete mode 100644 x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx delete mode 100644 x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts index e1700ad6a632d..b67a9c355e723 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/components/index.ts @@ -6,4 +6,9 @@ export { TabAliases, TabMappings, TabSettings } from './details_panel'; -export { StepAliases, StepMappings, StepSettings } from './wizard_steps'; +export { + StepAliasesContainer, + StepMappingsContainer, + StepSettingsContainer, + CommonWizardSteps, +} from './wizard_steps'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts index 90ce6227c09c8..ea554ca269d8b 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/index.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { StepAliases } from './step_aliases'; -export { StepMappings } from './step_mappings'; -export { StepSettings } from './step_settings'; +export { StepAliasesContainer } from './step_aliases_container'; +export { StepMappingsContainer } from './step_mappings_container'; +export { StepSettingsContainer } from './step_settings_container'; + +export { CommonWizardSteps } from './types'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx new file mode 100644 index 0000000000000..a5953ea00a106 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases_container.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +import { Forms } from '../../../../../shared_imports'; +import { CommonWizardSteps } from './types'; +import { StepAliases } from './step_aliases'; + +interface Props { + esDocsBase: string; +} + +export const StepAliasesContainer: React.FunctionComponent = ({ esDocsBase }) => { + const { defaultValue, updateContent } = Forms.useContent('aliases'); + + return ( + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx similarity index 57% rename from x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx rename to x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx index 80c0d1d4df489..34e05d88c651d 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_mappings_container.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings_container.tsx @@ -5,20 +5,23 @@ */ import React from 'react'; -import { Forms } from '../../../../shared_imports'; -import { documentationService } from '../../../services/documentation'; -import { StepMappings } from '../../shared'; -import { WizardContent } from '../template_form'; +import { Forms } from '../../../../../shared_imports'; +import { CommonWizardSteps } from './types'; +import { StepMappings } from './step_mappings'; -export const StepMappingsContainer = () => { - const { defaultValue, updateContent, getData } = Forms.useContent('mappings'); +interface Props { + esDocsBase: string; +} + +export const StepMappingsContainer: React.FunctionComponent = ({ esDocsBase }) => { + const { defaultValue, updateContent, getData } = Forms.useContent('mappings'); return ( ); }; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx new file mode 100644 index 0000000000000..c540ddceb95c2 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings_container.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +import { Forms } from '../../../../../shared_imports'; +import { CommonWizardSteps } from './types'; +import { StepSettings } from './step_settings'; + +interface Props { + esDocsBase: string; +} + +export const StepSettingsContainer = React.memo(({ esDocsBase }: Props) => { + const { defaultValue, updateContent } = Forms.useContent('settings'); + + return ( + + ); +}); diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts new file mode 100644 index 0000000000000..f8088e2b6e058 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Mappings, IndexSettings, Aliases } from '../../../../../../common'; + +export interface CommonWizardSteps { + settings?: IndexSettings; + mappings?: Mappings; + aliases?: Aliases; +} diff --git a/x-pack/plugins/index_management/public/application/components/shared/index.ts b/x-pack/plugins/index_management/public/application/components/shared/index.ts index 5ec1f71710270..897e86c99eca0 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/index.ts +++ b/x-pack/plugins/index_management/public/application/components/shared/index.ts @@ -8,7 +8,8 @@ export { TabAliases, TabMappings, TabSettings, - StepAliases, - StepMappings, - StepSettings, + StepAliasesContainer, + StepMappingsContainer, + StepSettingsContainer, + CommonWizardSteps, } from './components'; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts b/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts index 95d1222ad2cc9..b7e3e36e61814 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/index.ts @@ -5,7 +5,4 @@ */ export { StepLogisticsContainer } from './step_logistics_container'; -export { StepAliasesContainer } from './step_aliases_container'; -export { StepMappingsContainer } from './step_mappings_container'; -export { StepSettingsContainer } from './step_settings_container'; export { StepReviewContainer } from './step_review_container'; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx deleted file mode 100644 index a0e0c59be6622..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_aliases_container.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; - -import { Forms } from '../../../../shared_imports'; -import { documentationService } from '../../../services/documentation'; -import { StepAliases } from '../../shared'; -import { WizardContent } from '../template_form'; - -export const StepAliasesContainer = () => { - const { defaultValue, updateContent } = Forms.useContent('aliases'); - - return ( - - ); -}; diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx deleted file mode 100644 index b79c6804d382b..0000000000000 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_settings_container.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; - -import { Forms } from '../../../../shared_imports'; -import { documentationService } from '../../../services/documentation'; -import { StepSettings } from '../../shared'; -import { WizardContent } from '../template_form'; - -export const StepSettingsContainer = React.memo(() => { - const { defaultValue, updateContent } = Forms.useContent('settings'); - - return ( - - ); -}); diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 9e6d49faac563..8a2c991aea8d0 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -11,13 +11,14 @@ import { EuiSpacer } from '@elastic/eui'; import { TemplateDeserialized, CREATE_LEGACY_TEMPLATE_BY_DEFAULT } from '../../../../common'; import { serializers, Forms } from '../../../shared_imports'; import { SectionError } from '../section_error'; +import { StepLogisticsContainer, StepReviewContainer } from './steps'; import { - StepLogisticsContainer, + CommonWizardSteps, StepSettingsContainer, StepMappingsContainer, StepAliasesContainer, - StepReviewContainer, -} from './steps'; +} from '../shared'; +import { documentationService } from '../../services/documentation'; const { stripEmptyFields } = serializers; const { FormWizard, FormWizardStep } = Forms; @@ -31,11 +32,8 @@ interface Props { isEditing?: boolean; } -export interface WizardContent { +export interface WizardContent extends CommonWizardSteps { logistics: Omit; - settings: TemplateDeserialized['template']['settings']; - mappings: TemplateDeserialized['template']['mappings']; - aliases: TemplateDeserialized['template']['aliases']; } export type WizardSection = keyof WizardContent | 'review'; @@ -183,15 +181,15 @@ export const TemplateForm = ({ - + - + - + From 5bed09cd3251e1f464be9c7784e8d524d9ab4d8a Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 24 Jun 2020 13:29:43 -0500 Subject: [PATCH 08/82] Add lists plugin to optimized security_solution TS config (#69705) (#69823) As security_solution continues to integrate with lists, the absents of these types will lead to lots of implicit anys and false positives. Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../security_solution/scripts/optimize_tsconfig/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json b/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json index dcce9746086e0..ea7a11b89dab2 100644 --- a/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json +++ b/x-pack/plugins/security_solution/scripts/optimize_tsconfig/tsconfig.json @@ -1,6 +1,7 @@ { "include": [ "typings/**/*", + "plugins/lists/**/*", "plugins/security_solution/**/*", "plugins/apm/typings/numeral.d.ts", "plugins/canvas/types/webpack.d.ts", From 8935d79bdad42f42b433294136c466811eae480c Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 24 Jun 2020 13:31:39 -0500 Subject: [PATCH 09/82] [Security][Network] Exclude glob-only (*) Index Pattern from map layers (#69736) (#69824) * Exclude glob-only (*) index pattern from map layers This pattern is a special case that our map should ignore, as including it causes all indexes to be queried. * Ignore CCS glob pattern in our embedded map Users may have this pattern for cross-cluster search, and it should similarly be excluded when matching Security indexes. --- .../components/embeddables/__mocks__/mock.ts | 9 +++++++++ .../embeddables/embedded_map_helpers.test.tsx | 13 +++++++++++-- .../components/embeddables/embedded_map_helpers.tsx | 13 ++++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/network/components/embeddables/__mocks__/mock.ts index bc1de567b60ae..6f8c3e1123854 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/__mocks__/mock.ts @@ -475,3 +475,12 @@ export const mockGlobIndexPattern: IndexPatternSavedObject = { title: '*', }, }; + +export const mockCCSGlobIndexPattern: IndexPatternSavedObject = { + id: '*:*', + type: 'index-pattern', + _version: 'abc', + attributes: { + title: '*:*', + }, +}; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx index d42ac919e9af0..50170f4f6ae9e 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.test.tsx @@ -14,6 +14,7 @@ import { mockAuditbeatIndexPattern, mockFilebeatIndexPattern, mockGlobIndexPattern, + mockCCSGlobIndexPattern, } from './__mocks__/mock'; const mockEmbeddable = embeddablePluginMock.createStartContract(); @@ -106,12 +107,20 @@ describe('embedded_map_helpers', () => { ]); }); - test('finds glob-only index patterns ', () => { + test('excludes glob-only index patterns', () => { const matchingIndexPatterns = findMatchingIndexPatterns({ kibanaIndexPatterns: [mockGlobIndexPattern, mockFilebeatIndexPattern], siemDefaultIndices, }); - expect(matchingIndexPatterns).toEqual([mockGlobIndexPattern, mockFilebeatIndexPattern]); + expect(matchingIndexPatterns).toEqual([mockFilebeatIndexPattern]); + }); + + test('excludes glob-only CCS index patterns', () => { + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns: [mockCCSGlobIndexPattern, mockFilebeatIndexPattern], + siemDefaultIndices, + }); + expect(matchingIndexPatterns).toEqual([mockFilebeatIndexPattern]); }); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx index e50dcd7a8c8d8..b0f8e2cc02403 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map_helpers.tsx @@ -128,6 +128,9 @@ export const createEmbeddable = async ( return embeddableObject; }; +// These patterns are overly greedy and must be excluded when matching against Security indexes. +const ignoredIndexPatterns = ['*', '*:*']; + /** * Returns kibanaIndexPatterns that wildcard match at least one of siemDefaultIndices * @@ -142,9 +145,13 @@ export const findMatchingIndexPatterns = ({ siemDefaultIndices: string[]; }): IndexPatternSavedObject[] => { try { - return kibanaIndexPatterns.filter((kip) => - siemDefaultIndices.some((sdi) => minimatch(sdi, kip.attributes.title)) - ); + return kibanaIndexPatterns.filter((kip) => { + const pattern = kip.attributes.title; + return ( + !ignoredIndexPatterns.includes(pattern) && + siemDefaultIndices.some((sdi) => minimatch(sdi, pattern)) + ); + }); } catch { return []; } From eb94c7fbf92002c15190dbceec184ce0daf2e5ef Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Wed, 24 Jun 2020 12:45:45 -0600 Subject: [PATCH 10/82] [data.search.aggs] Move agg-specific field formats to search service (#69586) (#69825) --- .../public/field_formats/utils/deserialize.ts | 107 ++++------------ .../aggs/utils/get_format_with_aggs.test.ts | 99 +++++++++++++++ .../search/aggs/utils/get_format_with_aggs.ts | 116 ++++++++++++++++++ .../data/public/search/aggs/utils/index.ts | 1 + .../expressions/common/types/common.ts | 2 +- 5 files changed, 238 insertions(+), 87 deletions(-) create mode 100644 src/plugins/data/public/search/aggs/utils/get_format_with_aggs.test.ts create mode 100644 src/plugins/data/public/search/aggs/utils/get_format_with_aggs.ts diff --git a/src/plugins/data/public/field_formats/utils/deserialize.ts b/src/plugins/data/public/field_formats/utils/deserialize.ts index d9c713c8b1eb4..26baa5fdeb1e4 100644 --- a/src/plugins/data/public/field_formats/utils/deserialize.ts +++ b/src/plugins/data/public/field_formats/utils/deserialize.ts @@ -18,107 +18,42 @@ */ import { identity } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { convertDateRangeToString, DateRangeKey } from '../../search/aggs/buckets/lib/date_range'; -import { convertIPRangeToString, IpRangeKey } from '../../search/aggs/buckets/lib/ip_range'; + import { SerializedFieldFormat } from '../../../../expressions/common/types'; -import { FieldFormatId, FieldFormatsContentType, IFieldFormat } from '../..'; + import { FieldFormat } from '../../../common'; -import { DataPublicPluginStart } from '../../../public'; -import { getUiSettings } from '../../../public/services'; import { FormatFactory } from '../../../common/field_formats/utils'; - -interface TermsFieldFormatParams { - otherBucketLabel: string; - missingBucketLabel: string; - id: string; -} - -function isTermsFieldFormat( - serializedFieldFormat: SerializedFieldFormat -): serializedFieldFormat is SerializedFieldFormat { - return serializedFieldFormat.id === 'terms'; -} +import { DataPublicPluginStart, IFieldFormat } from '../../../public'; +import { getUiSettings } from '../../../public/services'; +import { getFormatWithAggs } from '../../search/aggs/utils'; const getConfig = (key: string, defaultOverride?: any): any => getUiSettings().get(key, defaultOverride); const DefaultFieldFormat = FieldFormat.from(identity); -const getFieldFormat = ( - fieldFormatsService: DataPublicPluginStart['fieldFormats'], - id?: FieldFormatId, - params: object = {} -): IFieldFormat => { - if (id) { - const Format = fieldFormatsService.getType(id); - - if (Format) { - return new Format(params, getConfig); - } - } - - return new DefaultFieldFormat(); -}; - export const deserializeFieldFormat: FormatFactory = function ( this: DataPublicPluginStart['fieldFormats'], - mapping?: SerializedFieldFormat + serializedFieldFormat?: SerializedFieldFormat ) { - if (!mapping) { + if (!serializedFieldFormat) { return new DefaultFieldFormat(); } - const { id } = mapping; - if (id === 'range') { - const RangeFormat = FieldFormat.from((range: any) => { - const nestedFormatter = mapping.params as SerializedFieldFormat; - const format = getFieldFormat(this, nestedFormatter.id, nestedFormatter.params); - const gte = '\u2265'; - const lt = '\u003c'; - return i18n.translate('data.aggTypes.buckets.ranges.rangesFormatMessage', { - defaultMessage: '{gte} {from} and {lt} {to}', - values: { - gte, - from: format.convert(range.gte), - lt, - to: format.convert(range.lt), - }, - }); - }); - return new RangeFormat(); - } else if (id === 'date_range') { - const nestedFormatter = mapping.params as SerializedFieldFormat; - const DateRangeFormat = FieldFormat.from((range: DateRangeKey) => { - const format = getFieldFormat(this, nestedFormatter.id, nestedFormatter.params); - return convertDateRangeToString(range, format.convert.bind(format)); - }); - return new DateRangeFormat(); - } else if (id === 'ip_range') { - const nestedFormatter = mapping.params as SerializedFieldFormat; - const IpRangeFormat = FieldFormat.from((range: IpRangeKey) => { - const format = getFieldFormat(this, nestedFormatter.id, nestedFormatter.params); - return convertIPRangeToString(range, format.convert.bind(format)); - }); - return new IpRangeFormat(); - } else if (isTermsFieldFormat(mapping) && mapping.params) { - const { params } = mapping; - const convert = (val: string, type: FieldFormatsContentType) => { - const format = getFieldFormat(this, params.id, mapping.params); - if (val === '__other__') { - return params.otherBucketLabel; - } - if (val === '__missing__') { - return params.missingBucketLabel; + const getFormat = (mapping: SerializedFieldFormat): IFieldFormat => { + const { id, params = {} } = mapping; + if (id) { + const Format = this.getType(id); + + if (Format) { + return new Format(params, getConfig); } + } + + return new DefaultFieldFormat(); + }; - return format.convert(val, type); - }; + // decorate getFormat to handle custom types created by aggs + const getFieldFormat = getFormatWithAggs(getFormat); - return { - convert, - getConverterFor: (type: FieldFormatsContentType) => (val: string) => convert(val, type), - } as IFieldFormat; - } else { - return getFieldFormat(this, id, mapping.params); - } + return getFieldFormat(serializedFieldFormat); }; diff --git a/src/plugins/data/public/search/aggs/utils/get_format_with_aggs.test.ts b/src/plugins/data/public/search/aggs/utils/get_format_with_aggs.test.ts new file mode 100644 index 0000000000000..3b440bc50c93b --- /dev/null +++ b/src/plugins/data/public/search/aggs/utils/get_format_with_aggs.test.ts @@ -0,0 +1,99 @@ +/* + * 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 { identity } from 'lodash'; + +import { SerializedFieldFormat } from '../../../../../expressions/common/types'; +import { FieldFormat } from '../../../../common'; +import { IFieldFormat } from '../../../../public'; + +import { getFormatWithAggs } from './get_format_with_aggs'; + +describe('getFormatWithAggs', () => { + let getFormat: jest.MockedFunction<(mapping: SerializedFieldFormat) => IFieldFormat>; + + beforeEach(() => { + getFormat = jest.fn().mockImplementation(() => { + const DefaultFieldFormat = FieldFormat.from(identity); + return new DefaultFieldFormat(); + }); + }); + + test('calls provided getFormat if no matching aggs exist', () => { + const mapping = { id: 'foo', params: {} }; + const getFieldFormat = getFormatWithAggs(getFormat); + getFieldFormat(mapping); + + expect(getFormat).toHaveBeenCalledTimes(1); + expect(getFormat).toHaveBeenCalledWith(mapping); + }); + + test('creates custom format for date_range', () => { + const mapping = { id: 'date_range', params: {} }; + const getFieldFormat = getFormatWithAggs(getFormat); + const format = getFieldFormat(mapping); + + expect(format.convert({ from: '2020-05-01', to: '2020-06-01' })).toBe( + '2020-05-01 to 2020-06-01' + ); + expect(format.convert({ to: '2020-06-01' })).toBe('Before 2020-06-01'); + expect(format.convert({ from: '2020-06-01' })).toBe('After 2020-06-01'); + expect(getFormat).toHaveBeenCalledTimes(3); + }); + + test('creates custom format for ip_range', () => { + const mapping = { id: 'ip_range', params: {} }; + const getFieldFormat = getFormatWithAggs(getFormat); + const format = getFieldFormat(mapping); + + expect(format.convert({ type: 'range', from: '10.0.0.1', to: '10.0.0.10' })).toBe( + '10.0.0.1 to 10.0.0.10' + ); + expect(format.convert({ type: 'range', to: '10.0.0.10' })).toBe('-Infinity to 10.0.0.10'); + expect(format.convert({ type: 'range', from: '10.0.0.10' })).toBe('10.0.0.10 to Infinity'); + format.convert({ type: 'mask', mask: '10.0.0.1/24' }); + expect(getFormat).toHaveBeenCalledTimes(4); + }); + + test('creates custom format for range', () => { + const mapping = { id: 'range', params: {} }; + const getFieldFormat = getFormatWithAggs(getFormat); + const format = getFieldFormat(mapping); + + expect(format.convert({ gte: 1, lt: 20 })).toBe('≥ 1 and < 20'); + expect(getFormat).toHaveBeenCalledTimes(1); + }); + + test('creates custom format for terms', () => { + const mapping = { + id: 'terms', + params: { + otherBucketLabel: 'other bucket', + missingBucketLabel: 'missing bucket', + }, + }; + const getFieldFormat = getFormatWithAggs(getFormat); + const format = getFieldFormat(mapping); + + expect(format.convert('machine.os.keyword')).toBe('machine.os.keyword'); + expect(format.convert('__other__')).toBe(mapping.params.otherBucketLabel); + expect(format.convert('__missing__')).toBe(mapping.params.missingBucketLabel); + expect(getFormat).toHaveBeenCalledTimes(3); + }); +}); diff --git a/src/plugins/data/public/search/aggs/utils/get_format_with_aggs.ts b/src/plugins/data/public/search/aggs/utils/get_format_with_aggs.ts new file mode 100644 index 0000000000000..e0db249c7cf86 --- /dev/null +++ b/src/plugins/data/public/search/aggs/utils/get_format_with_aggs.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; + +import { SerializedFieldFormat } from '../../../../../expressions/common/types'; +import { FieldFormat } from '../../../../common'; +import { FieldFormatsContentType, IFieldFormat } from '../../../../public'; +import { convertDateRangeToString, DateRangeKey } from '../buckets/lib/date_range'; +import { convertIPRangeToString, IpRangeKey } from '../buckets/lib/ip_range'; + +type GetFieldFormat = (mapping: SerializedFieldFormat) => IFieldFormat; + +/** + * Certain aggs have custom field formats that are not part of the field formats + * registry. This function will take the `getFormat` function which is used inside + * `deserializeFieldFormat` and decorate it with the additional custom formats + * that the field formats service doesn't know anything about. + * + * This function is internal to the data plugin, and only exists for use inside + * the field formats service. + * + * @internal + */ +export function getFormatWithAggs(getFieldFormat: GetFieldFormat): GetFieldFormat { + return (mapping) => { + const { id, params = {} } = mapping; + + const customFormats: Record IFieldFormat> = { + range: () => { + const RangeFormat = FieldFormat.from((range: any) => { + const nestedFormatter = params as SerializedFieldFormat; + const format = getFieldFormat({ + id: nestedFormatter.id, + params: nestedFormatter.params, + }); + const gte = '\u2265'; + const lt = '\u003c'; + return i18n.translate('data.aggTypes.buckets.ranges.rangesFormatMessage', { + defaultMessage: '{gte} {from} and {lt} {to}', + values: { + gte, + from: format.convert(range.gte), + lt, + to: format.convert(range.lt), + }, + }); + }); + return new RangeFormat(); + }, + date_range: () => { + const nestedFormatter = params as SerializedFieldFormat; + const DateRangeFormat = FieldFormat.from((range: DateRangeKey) => { + const format = getFieldFormat({ + id: nestedFormatter.id, + params: nestedFormatter.params, + }); + return convertDateRangeToString(range, format.convert.bind(format)); + }); + return new DateRangeFormat(); + }, + ip_range: () => { + const nestedFormatter = params as SerializedFieldFormat; + const IpRangeFormat = FieldFormat.from((range: IpRangeKey) => { + const format = getFieldFormat({ + id: nestedFormatter.id, + params: nestedFormatter.params, + }); + return convertIPRangeToString(range, format.convert.bind(format)); + }); + return new IpRangeFormat(); + }, + terms: () => { + const convert = (val: string, type: FieldFormatsContentType) => { + const format = getFieldFormat({ id: params.id, params }); + + if (val === '__other__') { + return params.otherBucketLabel; + } + if (val === '__missing__') { + return params.missingBucketLabel; + } + + return format.convert(val, type); + }; + + return { + convert, + getConverterFor: (type: FieldFormatsContentType) => (val: string) => convert(val, type), + } as IFieldFormat; + }, + }; + + if (!id || !(id in customFormats)) { + return getFieldFormat(mapping); + } + + return customFormats[id](); + }; +} diff --git a/src/plugins/data/public/search/aggs/utils/index.ts b/src/plugins/data/public/search/aggs/utils/index.ts index 169d872b17d3a..5a889ee9ead9d 100644 --- a/src/plugins/data/public/search/aggs/utils/index.ts +++ b/src/plugins/data/public/search/aggs/utils/index.ts @@ -18,5 +18,6 @@ */ export * from './calculate_auto_time_expression'; +export * from './get_format_with_aggs'; export * from './prop_filter'; export * from './to_angular_json'; diff --git a/src/plugins/expressions/common/types/common.ts b/src/plugins/expressions/common/types/common.ts index f532f9708940e..040979e4264b5 100644 --- a/src/plugins/expressions/common/types/common.ts +++ b/src/plugins/expressions/common/types/common.ts @@ -61,7 +61,7 @@ export type UnmappedTypeStrings = 'date' | 'filter'; * Is used to carry information about how to format data in * a data table as part of the column definition. */ -export interface SerializedFieldFormat { +export interface SerializedFieldFormat> { id?: string; params?: TParams; } From 17162394ffaf96c60e194feb8f614ad633c5e9c9 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 24 Jun 2020 16:20:53 -0400 Subject: [PATCH 11/82] [ML] DF Analytics Creation: add progress indicator (#69583) (#69837) * add progress indicator to creation wizard page * only show progress bar if job is started immediately * add title and switch to timeout * fix progress check * clean up interval on unmount * fix types * clear interval if stats undefined. show progress if job created --- .../components/create_step/create_step.tsx | 5 + .../components/create_step/progress_stats.tsx | 110 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx index 8d51848a25f50..0d1690cf17946 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx @@ -19,6 +19,7 @@ import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/us import { Messages } from '../shared'; import { ANALYTICS_STEPS } from '../../page'; import { BackToListPanel } from '../back_to_list_panel'; +import { ProgressStats } from './progress_stats'; interface Props extends CreateAnalyticsFormProps { step: ANALYTICS_STEPS; @@ -27,8 +28,10 @@ interface Props extends CreateAnalyticsFormProps { export const CreateStep: FC = ({ actions, state, step }) => { const { createAnalyticsJob, startAnalyticsJob } = actions; const { isAdvancedEditorValidJson, isJobCreated, isJobStarted, isValid, requestMessages } = state; + const { jobId } = state.form; const [checked, setChecked] = useState(true); + const [showProgress, setShowProgress] = useState(false); if (step !== ANALYTICS_STEPS.CREATE) return null; @@ -36,6 +39,7 @@ export const CreateStep: FC = ({ actions, state, step }) => { await createAnalyticsJob(); if (checked) { + setShowProgress(true); startAnalyticsJob(); } }; @@ -82,6 +86,7 @@ export const CreateStep: FC = ({ actions, state, step }) => { )} + {isJobCreated === true && showProgress && } {isJobCreated === true && } ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx new file mode 100644 index 0000000000000..8cee63d3c4c84 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../../../../../contexts/kibana'; +import { getDataFrameAnalyticsProgressPhase } from '../../../analytics_management/components/analytics_list/common'; +import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; +import { ml } from '../../../../../services/ml_api_service'; +import { DataFrameAnalyticsId } from '../../../../common/analytics'; + +export const PROGRESS_REFRESH_INTERVAL_MS = 1000; + +export const ProgressStats: FC<{ jobId: DataFrameAnalyticsId }> = ({ jobId }) => { + const [initialized, setInitialized] = useState(false); + const [currentProgress, setCurrentProgress] = useState< + | { + currentPhase: number; + progress: number; + totalPhases: number; + } + | undefined + >(undefined); + + const { + services: { notifications }, + } = useMlKibana(); + + useEffect(() => { + setInitialized(true); + }, []); + + useEffect(() => { + const interval = setInterval(async () => { + try { + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const jobStats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (jobStats !== undefined) { + const progressStats = getDataFrameAnalyticsProgressPhase(jobStats); + setCurrentProgress(progressStats); + if ( + progressStats.currentPhase === progressStats.totalPhases && + progressStats.progress === 100 + ) { + clearInterval(interval); + } + } else { + clearInterval(interval); + } + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('xpack.ml.dataframe.analytics.create.analyticsProgressErrorMessage', { + defaultMessage: 'An error occurred getting progress stats for analytics job {jobId}', + values: { jobId }, + }) + ); + clearInterval(interval); + } + }, PROGRESS_REFRESH_INTERVAL_MS); + + return () => clearInterval(interval); + }, [initialized]); + + if (currentProgress === undefined) return null; + + return ( + <> + + + + {i18n.translate('xpack.ml.dataframe.analytics.create.analyticsProgressTitle', { + defaultMessage: 'Progress', + })} + + + + + + + + {i18n.translate('xpack.ml.dataframe.analytics.create.analyticsProgressPhaseTitle', { + defaultMessage: 'Phase', + })}{' '} + {currentProgress.currentPhase}/{currentProgress.totalPhases} + + + + + + + + {`${currentProgress.progress}%`} + + + + ); +}; From 7371ad8b012ddf6896b2d5da00a144c00ac6013f Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Wed, 24 Jun 2020 14:22:08 -0600 Subject: [PATCH 12/82] [7.x] [Maps] New mappings: maps-telemetry -> maps (#69816) (#69833) --- x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts index 2512bf3094bcf..ad0b17af36dda 100644 --- a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts @@ -6,7 +6,7 @@ import { SavedObjectsType } from 'src/core/server'; export const mapsTelemetrySavedObjects: SavedObjectsType = { - name: 'maps-telemetry', + name: 'maps', hidden: false, namespaceType: 'agnostic', mappings: { From b0d97592f6ae637f112f68323d04348caced03ba Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 24 Jun 2020 15:30:48 -0700 Subject: [PATCH 13/82] [APM] Pulls legacy ML code from service maps and integrations (#69779) (#69852) * Pulls out existing ML integration from the service maps * - removes ML job creation flyout in integrations menu on the service details UI - removes ML searches and transforms in the transaction charts API - removes unused shared functions and types related to the legacy ML integration * removes unused translations for APM anomaly detection * Adds tags to TODOs for easy searching later # Conflicts: # x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_bucket_span_response.ts --- .../apm/common/ml_job_constants.test.ts | 38 +-- x-pack/plugins/apm/common/ml_job_constants.ts | 19 -- x-pack/plugins/apm/common/service_map.ts | 10 - .../TransactionSelect.tsx | 56 ---- .../MachineLearningFlyout/index.tsx | 167 ---------- .../MachineLearningFlyout/view.tsx | 264 --------------- .../ServiceIntegrations/index.tsx | 107 ++----- .../app/ServiceMap/Popover/Contents.tsx | 9 +- .../ServiceMap/Popover/anomaly_detection.tsx | 157 --------- .../app/TransactionOverview/index.tsx | 18 +- .../MachineLearningLinks/MLJobLink.test.tsx | 15 - .../Links/MachineLearningLinks/MLJobLink.tsx | 18 +- .../shared/charts/TransactionCharts/index.tsx | 11 +- x-pack/plugins/apm/public/services/rest/ml.ts | 123 ------- .../service_map/get_service_anomalies.test.ts | 40 --- .../lib/service_map/get_service_anomalies.ts | 166 ---------- .../server/lib/service_map/get_service_map.ts | 19 +- .../server/lib/service_map/ml_helpers.test.ts | 76 ----- .../apm/server/lib/service_map/ml_helpers.ts | 67 ---- .../transform_service_map_responses.test.ts | 7 - .../transform_service_map_responses.ts | 24 +- .../__snapshots__/fetcher.test.ts.snap | 68 ---- .../__snapshots__/index.test.ts.snap | 38 --- .../__snapshots__/transform.test.ts.snap | 33 -- .../charts/get_anomaly_data/fetcher.test.ts | 76 ----- .../charts/get_anomaly_data/fetcher.ts | 90 ------ .../get_anomaly_data/get_ml_bucket_size.ts | 65 ---- .../charts/get_anomaly_data/index.test.ts | 83 ----- .../charts/get_anomaly_data/index.ts | 39 +-- .../mock_responses/ml_anomaly_response.ts | 127 -------- .../mock_responses/ml_bucket_span_response.ts | 32 -- .../charts/get_anomaly_data/transform.test.ts | 303 ------------------ .../charts/get_anomaly_data/transform.ts | 126 -------- .../translations/translations/ja-JP.json | 20 -- .../translations/translations/zh-CN.json | 20 -- 35 files changed, 57 insertions(+), 2474 deletions(-) delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/Popover/anomaly_detection.tsx delete mode 100644 x-pack/plugins/apm/public/services/rest/ml.ts delete mode 100644 x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts delete mode 100644 x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/fetcher.test.ts.snap delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/index.test.ts.snap delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/transform.test.ts.snap delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_anomaly_response.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_bucket_span_response.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts diff --git a/x-pack/plugins/apm/common/ml_job_constants.test.ts b/x-pack/plugins/apm/common/ml_job_constants.test.ts index 45bb7133e852e..96e3ba826d201 100644 --- a/x-pack/plugins/apm/common/ml_job_constants.test.ts +++ b/x-pack/plugins/apm/common/ml_job_constants.test.ts @@ -4,45 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getMlJobId, - getMlPrefix, - getMlJobServiceName, - getSeverity, - severity, -} from './ml_job_constants'; +import { getSeverity, severity } from './ml_job_constants'; describe('ml_job_constants', () => { - it('getMlPrefix', () => { - expect(getMlPrefix('myServiceName')).toBe('myservicename-'); - expect(getMlPrefix('myServiceName', 'myTransactionType')).toBe( - 'myservicename-mytransactiontype-' - ); - }); - - it('getMlJobId', () => { - expect(getMlJobId('myServiceName')).toBe( - 'myservicename-high_mean_response_time' - ); - expect(getMlJobId('myServiceName', 'myTransactionType')).toBe( - 'myservicename-mytransactiontype-high_mean_response_time' - ); - expect(getMlJobId('my service name')).toBe( - 'my_service_name-high_mean_response_time' - ); - expect(getMlJobId('my service name', 'my transaction type')).toBe( - 'my_service_name-my_transaction_type-high_mean_response_time' - ); - }); - - describe('getMlJobServiceName', () => { - it('extracts the service name from a job id', () => { - expect( - getMlJobServiceName('opbeans-node-request-high_mean_response_time') - ).toEqual('opbeans-node'); - }); - }); - describe('getSeverity', () => { describe('when score is undefined', () => { it('returns undefined', () => { diff --git a/x-pack/plugins/apm/common/ml_job_constants.ts b/x-pack/plugins/apm/common/ml_job_constants.ts index f9b0119d8a107..b8c2546bd0c84 100644 --- a/x-pack/plugins/apm/common/ml_job_constants.ts +++ b/x-pack/plugins/apm/common/ml_job_constants.ts @@ -11,25 +11,6 @@ export enum severity { warning = 'warning', } -export const APM_ML_JOB_GROUP_NAME = 'apm'; - -export function getMlPrefix(serviceName: string, transactionType?: string) { - const maybeTransactionType = transactionType ? `${transactionType}-` : ''; - return encodeForMlApi(`${serviceName}-${maybeTransactionType}`); -} - -export function getMlJobId(serviceName: string, transactionType?: string) { - return `${getMlPrefix(serviceName, transactionType)}high_mean_response_time`; -} - -export function getMlJobServiceName(jobId: string) { - return jobId.split('-').slice(0, -2).join('-'); -} - -export function encodeForMlApi(value: string) { - return value.replace(/\s+/g, '_').toLowerCase(); -} - export function getSeverity(score?: number) { if (typeof score !== 'number') { return undefined; diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 7d7a7811eeba2..43f3585d0ebb2 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -34,16 +34,6 @@ export interface Connection { destination: ConnectionNode; } -export interface ServiceAnomaly { - anomaly_score: number; - anomaly_severity: string; - actual_value: number; - typical_value: number; - ml_job_id: string; -} - -export type ServiceNode = ConnectionNode & Partial; - export interface ServiceNodeMetrics { avgMemoryUsage: number | null; avgCpuUsage: number | null; diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx deleted file mode 100644 index 42f7246b6ea35..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx +++ /dev/null @@ -1,56 +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 { - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiSuperSelect, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; - -interface TransactionSelectProps { - transactionTypes: string[]; - onChange: (value: string) => void; - selectedTransactionType: string; -} - -export function TransactionSelect({ - transactionTypes, - onChange, - selectedTransactionType, -}: TransactionSelectProps) { - return ( - - { - return { - value: transactionType, - inputDisplay: transactionType, - dropdownDisplay: ( - - - {transactionType} - - - ), - }; - })} - /> - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx deleted file mode 100644 index 91778b2940c6b..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import React, { Component } from 'react'; -import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { startMLJob, MLError } from '../../../../../services/rest/ml'; -import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; -import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink'; -import { MachineLearningFlyoutView } from './view'; -import { ApmPluginContext } from '../../../../../context/ApmPluginContext'; - -interface Props { - isOpen: boolean; - onClose: () => void; - urlParams: IUrlParams; -} - -interface State { - isCreatingJob: boolean; -} - -export class MachineLearningFlyout extends Component { - static contextType = ApmPluginContext; - - public state: State = { - isCreatingJob: false, - }; - - public onClickCreate = async ({ - transactionType, - }: { - transactionType: string; - }) => { - this.setState({ isCreatingJob: true }); - try { - const { http } = this.context.core; - const { serviceName } = this.props.urlParams; - if (!serviceName) { - throw new Error('Service name is required to create this ML job'); - } - const res = await startMLJob({ http, serviceName, transactionType }); - const didSucceed = res.datafeeds[0].success && res.jobs[0].success; - if (!didSucceed) { - throw new Error('Creating ML job failed'); - } - this.addSuccessToast({ transactionType }); - } catch (e) { - this.addErrorToast(e as MLError); - } - - this.setState({ isCreatingJob: false }); - this.props.onClose(); - }; - - public addErrorToast = (error: MLError) => { - const { core } = this.context; - - const { urlParams } = this.props; - const { serviceName } = urlParams; - - if (!serviceName) { - return; - } - - const errorDescription = error?.body?.message; - const errorText = errorDescription - ? `${error.message}: ${errorDescription}` - : error.message; - - core.notifications.toasts.addWarning({ - title: i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle', - { - defaultMessage: 'Job creation failed', - } - ), - text: toMountPoint( - <> -

{errorText}

-

- {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationText', - { - defaultMessage: - 'Your current license may not allow for creating machine learning jobs, or this job may already exist.', - } - )} -

- - ), - }); - }; - - public addSuccessToast = ({ - transactionType, - }: { - transactionType: string; - }) => { - const { core } = this.context; - const { urlParams } = this.props; - const { serviceName } = urlParams; - - if (!serviceName) { - return; - } - - core.notifications.toasts.addSuccess({ - title: i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationTitle', - { - defaultMessage: 'Job successfully created', - } - ), - text: toMountPoint( -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText', - { - defaultMessage: - 'The analysis is now running for {serviceName} ({transactionType}). It might take a while before results are added to the response times graph.', - values: { - serviceName, - transactionType, - }, - } - )}{' '} - - - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText', - { - defaultMessage: 'View job', - } - )} - - -

- ), - }); - }; - - public render() { - const { isOpen, onClose, urlParams } = this.props; - const { serviceName } = urlParams; - const { isCreatingJob } = this.state; - - if (!isOpen || !serviceName) { - return null; - } - - return ( - - ); - } -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx deleted file mode 100644 index 72e8193ba2de2..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx +++ /dev/null @@ -1,264 +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 { - EuiButton, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiFormRow, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useState, useEffect } from 'react'; -import { isEmpty } from 'lodash'; -import { FETCH_STATUS, useFetcher } from '../../../../../hooks/useFetcher'; -import { getHasMLJob } from '../../../../../services/rest/ml'; -import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink'; -import { MLLink } from '../../../../shared/Links/MachineLearningLinks/MLLink'; -import { TransactionSelect } from './TransactionSelect'; -import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; -import { useServiceTransactionTypes } from '../../../../../hooks/useServiceTransactionTypes'; -import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; - -interface Props { - isCreatingJob: boolean; - onClickCreate: ({ transactionType }: { transactionType: string }) => void; - onClose: () => void; - urlParams: IUrlParams; -} - -export function MachineLearningFlyoutView({ - isCreatingJob, - onClickCreate, - onClose, - urlParams, -}: Props) { - const { serviceName } = urlParams; - const transactionTypes = useServiceTransactionTypes(urlParams); - - const [selectedTransactionType, setSelectedTransactionType] = useState< - string | undefined - >(undefined); - - const { http } = useApmPluginContext().core; - - const { data: hasMLJob, status } = useFetcher( - () => { - if (serviceName && selectedTransactionType) { - return getHasMLJob({ - serviceName, - transactionType: selectedTransactionType, - http, - }); - } - }, - [serviceName, selectedTransactionType, http], - { showToastOnError: false } - ); - - // update selectedTransactionType when list of transaction types has loaded - useEffect(() => { - setSelectedTransactionType(transactionTypes[0]); - }, [transactionTypes]); - - if (!serviceName || !selectedTransactionType || isEmpty(transactionTypes)) { - return null; - } - - const isLoadingMLJob = status === FETCH_STATUS.LOADING; - const isMlAvailable = status !== FETCH_STATUS.FAILURE; - - return ( - - - -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle', - { - defaultMessage: 'Enable anomaly detection', - } - )} -

-
- -
- - {!isMlAvailable && ( -
- -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.mlNotAvailableDescription', - { - defaultMessage: - 'Unable to connect to Machine learning. Make sure it is enabled in Kibana to use anomaly detection.', - } - )} -

-
- -
- )} - {hasMLJob && ( -
- -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription', - { - defaultMessage: - 'There is currently a job running for {serviceName} ({transactionType}).', - values: { - serviceName, - transactionType: selectedTransactionType, - }, - } - )}{' '} - - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText', - { - defaultMessage: 'View existing job', - } - )} - -

-
- -
- )} - -

- - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText', - { - defaultMessage: 'transaction duration', - } - )} - - ), - serviceMapAnnotationText: ( - - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.serviceMapAnnotationText', - { - defaultMessage: 'service maps', - } - )} - - ), - }} - /> -

-

- - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText', - { - defaultMessage: 'Machine Learning Job Management page', - } - )} - - ), - }} - />{' '} - - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.noteText', - { - defaultMessage: - 'Note: It might take a few minutes for the job to begin calculating results.', - } - )} - -

-
- - -
- - - - {transactionTypes.length > 1 ? ( - { - setSelectedTransactionType(value); - }} - /> - ) : null} - - - - - onClickCreate({ transactionType: selectedTransactionType }) - } - fill - disabled={isCreatingJob || hasMLJob || isLoadingMLJob} - > - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel', - { - defaultMessage: 'Create job', - } - )} - - - - - -
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx index 321617ed8496a..0a7dcbd0be3df 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx @@ -4,18 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - EuiContextMenu, - EuiContextMenuPanelItemDescriptor, - EuiPopover, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiContextMenu, EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { memoize } from 'lodash'; -import React, { Fragment } from 'react'; +import React from 'react'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { LicenseContext } from '../../../../context/LicenseContext'; -import { MachineLearningFlyout } from './MachineLearningFlyout'; import { WatcherFlyout } from './WatcherFlyout'; import { ApmPluginContext } from '../../../../context/ApmPluginContext'; @@ -26,7 +18,7 @@ interface State { isPopoverOpen: boolean; activeFlyout: FlyoutName; } -type FlyoutName = null | 'ML' | 'Watcher'; +type FlyoutName = null | 'Watcher'; export class ServiceIntegrations extends React.Component { static contextType = ApmPluginContext; @@ -34,38 +26,6 @@ export class ServiceIntegrations extends React.Component { public state: State = { isPopoverOpen: false, activeFlyout: null }; - public getPanelItems = memoize((mlAvailable: boolean | undefined) => { - let panelItems: EuiContextMenuPanelItemDescriptor[] = []; - if (mlAvailable) { - panelItems = panelItems.concat(this.getMLPanelItems()); - } - return panelItems.concat(this.getWatcherPanelItems()); - }); - - public getMLPanelItems = () => { - return [ - { - name: i18n.translate( - 'xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonLabel', - { - defaultMessage: 'Enable ML anomaly detection', - } - ), - icon: 'machineLearningApp', - toolTipContent: i18n.translate( - 'xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonTooltip', - { - defaultMessage: 'Set up a machine learning job for this service', - } - ), - onClick: () => { - this.closePopover(); - this.openFlyout('ML'); - }, - }, - ]; - }; - public getWatcherPanelItems = () => { const { core } = this.context; @@ -132,42 +92,31 @@ export class ServiceIntegrations extends React.Component { ); return ( - - {(license) => ( - - - - - - - - )} - + <> + + + + + ); } } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index ff68288916af4..78779bdcc2052 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -15,8 +15,6 @@ import React, { MouseEvent } from 'react'; import { Buttons } from './Buttons'; import { Info } from './Info'; import { ServiceMetricFetcher } from './ServiceMetricFetcher'; -import { AnomalyDetection } from './anomaly_detection'; -import { ServiceNode } from '../../../../../common/service_map'; import { popoverMinWidth } from '../cytoscapeOptions'; interface ContentsProps { @@ -70,12 +68,13 @@ export function Contents({ - {isService && ( + {/* //TODO [APM ML] add service health stats here: + isService && ( - + - )} + )*/} {isService ? ( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/anomaly_detection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/anomaly_detection.tsx deleted file mode 100644 index 531bbb139d58b..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/anomaly_detection.tsx +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import styled from 'styled-components'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiIconTip, - EuiHealth, -} from '@elastic/eui'; -import { useTheme } from '../../../../hooks/useTheme'; -import { fontSize, px } from '../../../../style/variables'; -import { asInteger } from '../../../../utils/formatters'; -import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; -import { getSeverityColor, popoverMinWidth } from '../cytoscapeOptions'; -import { getMetricChangeDescription } from '../../../../../../ml/public'; -import { ServiceNode } from '../../../../../common/service_map'; - -const HealthStatusTitle = styled(EuiTitle)` - display: inline; - text-transform: uppercase; -`; - -const VerticallyCentered = styled.div` - display: flex; - align-items: center; -`; - -const SubduedText = styled.span` - color: ${({ theme }) => theme.eui.euiTextSubduedColor}; -`; - -const EnableText = styled.section` - color: ${({ theme }) => theme.eui.euiTextSubduedColor}; - line-height: 1.4; - font-size: ${fontSize}; - width: ${px(popoverMinWidth)}; -`; - -export const ContentLine = styled.section` - line-height: 2; -`; - -interface AnomalyDetectionProps { - serviceNodeData: cytoscape.NodeDataDefinition & ServiceNode; -} - -export function AnomalyDetection({ serviceNodeData }: AnomalyDetectionProps) { - const theme = useTheme(); - const anomalySeverity = serviceNodeData.anomaly_severity; - const anomalyScore = serviceNodeData.anomaly_score; - const actualValue = serviceNodeData.actual_value; - const typicalValue = serviceNodeData.typical_value; - const mlJobId = serviceNodeData.ml_job_id; - const hasAnomalyDetectionScore = - anomalySeverity !== undefined && anomalyScore !== undefined; - const anomalyDescription = - hasAnomalyDetectionScore && - actualValue !== undefined && - typicalValue !== undefined - ? getMetricChangeDescription(actualValue, typicalValue).message - : null; - - return ( - <> -
- -

{ANOMALY_DETECTION_TITLE}

-
-   - - {!mlJobId && {ANOMALY_DETECTION_DISABLED_TEXT}} -
- {hasAnomalyDetectionScore && ( - - - - - - {ANOMALY_DETECTION_SCORE_METRIC} - - - -
- {getDisplayedAnomalyScore(anomalyScore as number)} - {anomalyDescription && ( -  ({anomalyDescription}) - )} -
-
-
-
- )} - {mlJobId && !hasAnomalyDetectionScore && ( - {ANOMALY_DETECTION_NO_DATA_TEXT} - )} - {mlJobId && ( - - - {ANOMALY_DETECTION_LINK} - - - )} - - ); -} - -function getDisplayedAnomalyScore(score: number) { - if (score > 0 && score < 1) { - return '< 1'; - } - return asInteger(score); -} - -const ANOMALY_DETECTION_TITLE = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverTitle', - { defaultMessage: 'Anomaly Detection' } -); - -const ANOMALY_DETECTION_TOOLTIP = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverTooltip', - { - defaultMessage: - 'Service health indicators are powered by the anomaly detection feature in machine learning', - } -); - -const ANOMALY_DETECTION_SCORE_METRIC = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric', - { defaultMessage: 'Score (max.)' } -); - -const ANOMALY_DETECTION_LINK = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverLink', - { defaultMessage: 'View anomalies' } -); - -const ANOMALY_DETECTION_DISABLED_TEXT = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverDisabled', - { - defaultMessage: - 'Display service health indicators by enabling anomaly detection from the Integrations menu in the Service details view.', - } -); - -const ANOMALY_DETECTION_NO_DATA_TEXT = i18n.translate( - 'xpack.apm.serviceMap.anomalyDetectionPopoverNoData', - { - defaultMessage: `We couldn't find an anomaly score within the selected time range. See details in the anomaly explorer.`, - } -); diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 9018fbb2bc410..fc5347d081316 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -22,8 +22,6 @@ import { TransactionCharts } from '../../shared/charts/TransactionCharts'; import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; import { TransactionList } from './List'; import { useRedirect } from './useRedirect'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { getHasMLJob } from '../../../services/rest/ml'; import { history } from '../../../utils/history'; import { useLocation } from '../../../hooks/useLocation'; import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; @@ -34,7 +32,6 @@ import { PROJECTION } from '../../../../common/projections/typings'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; function getRedirectLocation({ urlParams, @@ -86,18 +83,6 @@ export function TransactionOverview() { status: transactionListStatus, } = useTransactionList(urlParams); - const { http } = useApmPluginContext().core; - - const { data: hasMLJob = false } = useFetcher( - () => { - if (serviceName && transactionType) { - return getHasMLJob({ serviceName, transactionType, http }); - } - }, - [http, serviceName, transactionType], - { showToastOnError: false } - ); - const localFiltersConfig: React.ComponentProps = useMemo( () => ({ filterNames: [ @@ -140,7 +125,8 @@ export function TransactionOverview() { { - it('should produce the correct URL with serviceName', async () => { - const href = await getRenderedHref( - () => ( - - ), - { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location - ); - - expect(href).toEqual( - `/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))` - ); - }); it('should produce the correct URL with jobId', async () => { const href = await getRenderedHref( () => ( diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx index 346748964d529..1e1f9ea5f23b7 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx @@ -5,28 +5,16 @@ */ import React from 'react'; -import { getMlJobId } from '../../../../../common/ml_job_constants'; import { MLLink } from './MLLink'; -interface PropsServiceName { - serviceName: string; - transactionType?: string; -} -interface PropsJobId { +interface Props { jobId: string; -} - -type Props = (PropsServiceName | PropsJobId) & { external?: boolean; -}; +} export const MLJobLink: React.FC = (props) => { - const jobId = - 'jobId' in props - ? props.jobId - : getMlJobId(props.serviceName, props.transactionType); const query = { - ml: { jobIds: [jobId] }, + ml: { jobIds: [props.jobId] }, }; return ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 4821e06419e34..00ff6f9969725 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -101,11 +101,13 @@ export class TransactionCharts extends Component { return null; } - const { serviceName, transactionType, kuery } = this.props.urlParams; + const { serviceName, kuery } = this.props.urlParams; if (!serviceName) { return null; } + const linkedJobId = ''; // TODO [APM ML] link to ML job id for the selected environment + const hasKuery = !isEmpty(kuery); const icon = hasKuery ? ( { } )}{' '} - - View Job - + View Job ); diff --git a/x-pack/plugins/apm/public/services/rest/ml.ts b/x-pack/plugins/apm/public/services/rest/ml.ts deleted file mode 100644 index 47032501d9fbe..0000000000000 --- a/x-pack/plugins/apm/public/services/rest/ml.ts +++ /dev/null @@ -1,123 +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 { HttpSetup } from 'kibana/public'; -import { - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_TYPE, -} from '../../../common/elasticsearch_fieldnames'; -import { - APM_ML_JOB_GROUP_NAME, - getMlJobId, - getMlPrefix, - encodeForMlApi, -} from '../../../common/ml_job_constants'; -import { callApi } from './callApi'; -import { ESFilter } from '../../../typings/elasticsearch'; -import { callApmApi } from './createCallApmApi'; - -interface MlResponseItem { - id: string; - success: boolean; - error?: { - msg: string; - body: string; - path: string; - response: string; - statusCode: number; - }; -} - -interface StartedMLJobApiResponse { - datafeeds: MlResponseItem[]; - jobs: MlResponseItem[]; -} - -async function getTransactionIndices() { - const indices = await callApmApi({ - method: 'GET', - pathname: `/api/apm/settings/apm-indices`, - }); - return indices['apm_oss.transactionIndices']; -} - -export async function startMLJob({ - serviceName, - transactionType, - http, -}: { - serviceName: string; - transactionType: string; - http: HttpSetup; -}) { - const transactionIndices = await getTransactionIndices(); - const groups = [ - APM_ML_JOB_GROUP_NAME, - encodeForMlApi(serviceName), - encodeForMlApi(transactionType), - ]; - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - ]; - return callApi(http, { - method: 'POST', - pathname: `/api/ml/modules/setup/apm_transaction`, - body: { - prefix: getMlPrefix(serviceName, transactionType), - groups, - indexPatternName: transactionIndices, - startDatafeed: true, - query: { - bool: { - filter, - }, - }, - }, - }); -} - -// https://www.elastic.co/guide/en/elasticsearch/reference/6.5/ml-get-job.html -export interface MLJobApiResponse { - count: number; - jobs: Array<{ - job_id: string; - }>; -} - -export type MLError = Error & { body?: { message?: string } }; - -export async function getHasMLJob({ - serviceName, - transactionType, - http, -}: { - serviceName: string; - transactionType: string; - http: HttpSetup; -}) { - try { - await callApi(http, { - method: 'GET', - pathname: `/api/ml/anomaly_detectors/${getMlJobId( - serviceName, - transactionType - )}`, - }); - return true; - } catch (error) { - if ( - error?.body?.statusCode === 404 && - error?.body?.attributes?.body?.error?.type === - 'resource_not_found_exception' - ) { - return false; // false only if ML api responds with resource_not_found_exception - } - throw error; - } -} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.test.ts deleted file mode 100644 index aefd074c373f9..0000000000000 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getApmMlJobCategory } from './get_service_anomalies'; -import { Job as AnomalyDetectionJob } from '../../../../ml/server'; - -describe('getApmMlJobCategory', () => { - it('should match service names with different casings', () => { - const mlJob = { - job_id: 'testservice-request-high_mean_response_time', - groups: ['apm', 'testservice', 'request'], - } as AnomalyDetectionJob; - const serviceNames = ['testService']; - const apmMlJobCategory = getApmMlJobCategory(mlJob, serviceNames); - - expect(apmMlJobCategory).toEqual({ - jobId: 'testservice-request-high_mean_response_time', - serviceName: 'testService', - transactionType: 'request', - }); - }); - - it('should match service names with spaces', () => { - const mlJob = { - job_id: 'test_service-request-high_mean_response_time', - groups: ['apm', 'test_service', 'request'], - } as AnomalyDetectionJob; - const serviceNames = ['Test Service']; - const apmMlJobCategory = getApmMlJobCategory(mlJob, serviceNames); - - expect(apmMlJobCategory).toEqual({ - jobId: 'test_service-request-high_mean_response_time', - serviceName: 'Test Service', - transactionType: 'request', - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts deleted file mode 100644 index 900141e9040ae..0000000000000 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { intersection } from 'lodash'; -import { leftJoin } from '../../../common/utils/left_join'; -import { Job as AnomalyDetectionJob } from '../../../../ml/server'; -import { PromiseReturnType } from '../../../typings/common'; -import { IEnvOptions } from './get_service_map'; -import { Setup } from '../helpers/setup_request'; -import { - APM_ML_JOB_GROUP_NAME, - encodeForMlApi, -} from '../../../common/ml_job_constants'; - -async function getApmAnomalyDetectionJobs( - setup: Setup -): Promise { - const { ml } = setup; - - if (!ml) { - return []; - } - try { - const { jobs } = await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP_NAME); - return jobs; - } catch (error) { - if (error.statusCode === 404) { - return []; - } - throw error; - } -} - -type ApmMlJobCategory = NonNullable>; - -export const getApmMlJobCategory = ( - mlJob: AnomalyDetectionJob, - serviceNames: string[] -) => { - const serviceByGroupNameMap = new Map( - serviceNames.map((serviceName) => [ - encodeForMlApi(serviceName), - serviceName, - ]) - ); - if (!mlJob.groups.includes(APM_ML_JOB_GROUP_NAME)) { - // ML job missing "apm" group name - return; - } - const apmJobGroups = mlJob.groups.filter( - (groupName) => groupName !== APM_ML_JOB_GROUP_NAME - ); - const apmJobServiceNames = apmJobGroups.map( - (groupName) => serviceByGroupNameMap.get(groupName) || groupName - ); - const [serviceName] = intersection(apmJobServiceNames, serviceNames); - if (!serviceName) { - // APM ML job service was not found - return; - } - const serviceGroupName = encodeForMlApi(serviceName); - const [transactionType] = apmJobGroups.filter( - (groupName) => groupName !== serviceGroupName - ); - if (!transactionType) { - // APM ML job transaction type was not found. - return; - } - return { jobId: mlJob.job_id, serviceName, transactionType }; -}; - -export type ServiceAnomalies = PromiseReturnType; - -export async function getServiceAnomalies( - options: IEnvOptions, - serviceNames: string[] -) { - const { start, end, ml } = options.setup; - - if (!ml || serviceNames.length === 0) { - return []; - } - - const apmMlJobs = await getApmAnomalyDetectionJobs(options.setup); - if (apmMlJobs.length === 0) { - return []; - } - const apmMlJobCategories = apmMlJobs - .map((job) => getApmMlJobCategory(job, serviceNames)) - .filter( - (apmJobCategory) => apmJobCategory !== undefined - ) as ApmMlJobCategory[]; - const apmJobIds = apmMlJobs.map((job) => job.job_id); - const params = { - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { result_type: 'record' } }, - { - terms: { - job_id: apmJobIds, - }, - }, - { - range: { - timestamp: { gte: start, lte: end, format: 'epoch_millis' }, - }, - }, - ], - }, - }, - aggs: { - jobs: { - terms: { field: 'job_id', size: apmJobIds.length }, - aggs: { - top_score_hits: { - top_hits: { - sort: [{ record_score: { order: 'desc' as const } }], - _source: ['record_score', 'timestamp', 'typical', 'actual'], - size: 1, - }, - }, - }, - }, - }, - }, - }; - - const response = (await ml.mlSystem.mlAnomalySearch(params)) as { - aggregations: { - jobs: { - buckets: Array<{ - key: string; - top_score_hits: { - hits: { - hits: Array<{ - _source: { - record_score: number; - timestamp: number; - typical: number[]; - actual: number[]; - }; - }>; - }; - }; - }>; - }; - }; - }; - const anomalyScores = response.aggregations.jobs.buckets.map((jobBucket) => { - const jobId = jobBucket.key; - const bucketSource = jobBucket.top_score_hits.hits.hits?.[0]?._source; - return { - jobId, - anomalyScore: bucketSource.record_score, - timestamp: bucketSource.timestamp, - typical: bucketSource.typical[0], - actual: bucketSource.actual[0], - }; - }); - return leftJoin(apmMlJobCategories, 'jobId', anomalyScores); -} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 9f3ded82d7cbd..4d488cd1a5509 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -13,14 +13,9 @@ import { getServicesProjection } from '../../../common/projections/services'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { PromiseReturnType } from '../../../typings/common'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { - transformServiceMapResponses, - getAllNodes, - getServiceNodes, -} from './transform_service_map_responses'; +import { transformServiceMapResponses } from './transform_service_map_responses'; import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; -import { getServiceAnomalies, ServiceAnomalies } from './get_service_anomalies'; export interface IEnvOptions { setup: Setup & SetupTimeRange; @@ -132,7 +127,6 @@ async function getServicesData(options: IEnvOptions) { ); } -export { ServiceAnomalies }; export type ConnectionsResponse = PromiseReturnType; export type ServicesResponse = PromiseReturnType; export type ServiceMapAPIResponse = PromiseReturnType; @@ -143,19 +137,8 @@ export async function getServiceMap(options: IEnvOptions) { getServicesData(options), ]); - // Derive all related service names from connection and service data - const allNodes = getAllNodes(servicesData, connectionData.connections); - const serviceNodes = getServiceNodes(allNodes); - const serviceNames = serviceNodes.map( - (serviceData) => serviceData[SERVICE_NAME] - ); - - // Get related service anomalies - const serviceAnomalies = await getServiceAnomalies(options, serviceNames); - return transformServiceMapResponses({ ...connectionData, - anomalies: serviceAnomalies, services: servicesData, }); } diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts deleted file mode 100644 index f07b575cc0a35..0000000000000 --- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ServiceAnomalies } from './get_service_map'; -import { addAnomaliesDataToNodes } from './ml_helpers'; - -describe('addAnomaliesDataToNodes', () => { - it('adds anomalies to nodes', () => { - const nodes = [ - { - 'service.name': 'opbeans-ruby', - 'agent.name': 'ruby', - 'service.environment': null, - }, - { - 'service.name': 'opbeans-java', - 'agent.name': 'java', - 'service.environment': null, - }, - ]; - - const serviceAnomalies: ServiceAnomalies = [ - { - jobId: 'opbeans-ruby-request-high_mean_response_time', - serviceName: 'opbeans-ruby', - transactionType: 'request', - anomalyScore: 50, - timestamp: 1591351200000, - actual: 2000, - typical: 1000, - }, - { - jobId: 'opbeans-java-request-high_mean_response_time', - serviceName: 'opbeans-java', - transactionType: 'request', - anomalyScore: 100, - timestamp: 1591351200000, - actual: 9000, - typical: 3000, - }, - ]; - - const result = [ - { - 'service.name': 'opbeans-ruby', - 'agent.name': 'ruby', - 'service.environment': null, - anomaly_score: 50, - anomaly_severity: 'major', - actual_value: 2000, - typical_value: 1000, - ml_job_id: 'opbeans-ruby-request-high_mean_response_time', - }, - { - 'service.name': 'opbeans-java', - 'agent.name': 'java', - 'service.environment': null, - anomaly_score: 100, - anomaly_severity: 'critical', - actual_value: 9000, - typical_value: 3000, - ml_job_id: 'opbeans-java-request-high_mean_response_time', - }, - ]; - - expect( - addAnomaliesDataToNodes( - nodes, - (serviceAnomalies as unknown) as ServiceAnomalies - ) - ).toEqual(result); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts deleted file mode 100644 index 8162417616b6c..0000000000000 --- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts +++ /dev/null @@ -1,67 +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 { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; -import { getSeverity } from '../../../common/ml_job_constants'; -import { ConnectionNode, ServiceNode } from '../../../common/service_map'; -import { ServiceAnomalies } from './get_service_map'; - -export function addAnomaliesDataToNodes( - nodes: ConnectionNode[], - serviceAnomalies: ServiceAnomalies -) { - const anomaliesMap = serviceAnomalies.reduce( - (acc, anomalyJob) => { - const serviceAnomaly: typeof acc[string] | undefined = - acc[anomalyJob.serviceName]; - const hasAnomalyJob = serviceAnomaly !== undefined; - const hasAnomalyScore = serviceAnomaly?.anomaly_score !== undefined; - const hasNewAnomalyScore = anomalyJob.anomalyScore !== undefined; - const hasNewMaxAnomalyScore = - hasNewAnomalyScore && - (!hasAnomalyScore || - (anomalyJob?.anomalyScore ?? 0) > - (serviceAnomaly?.anomaly_score ?? 0)); - - if (!hasAnomalyJob || hasNewMaxAnomalyScore) { - acc[anomalyJob.serviceName] = { - anomaly_score: anomalyJob.anomalyScore, - actual_value: anomalyJob.actual, - typical_value: anomalyJob.typical, - ml_job_id: anomalyJob.jobId, - }; - } - - return acc; - }, - {} as { - [serviceName: string]: { - anomaly_score?: number; - actual_value?: number; - typical_value?: number; - ml_job_id: string; - }; - } - ); - - const servicesDataWithAnomalies: ServiceNode[] = nodes.map((service) => { - const serviceAnomaly = anomaliesMap[service[SERVICE_NAME]]; - if (serviceAnomaly) { - const anomalyScore = serviceAnomaly.anomaly_score; - return { - ...service, - anomaly_score: anomalyScore, - anomaly_severity: getSeverity(anomalyScore), - actual_value: serviceAnomaly.actual_value, - typical_value: serviceAnomaly.typical_value, - ml_job_id: serviceAnomaly.ml_job_id, - }; - } - return service; - }); - - return servicesDataWithAnomalies; -} diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index 6c9880c2dc4df..1e26634bdf0f1 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -12,7 +12,6 @@ import { SPAN_SUBTYPE, SPAN_TYPE, } from '../../../common/elasticsearch_fieldnames'; -import { ServiceAnomalies } from './get_service_map'; import { transformServiceMapResponses, ServiceMapResponse, @@ -36,12 +35,9 @@ const javaService = { [AGENT_NAME]: 'java', }; -const serviceAnomalies: ServiceAnomalies = []; - describe('transformServiceMapResponses', () => { it('maps external destinations to internal services', () => { const response: ServiceMapResponse = { - anomalies: serviceAnomalies, services: [nodejsService, javaService], discoveredServices: [ { @@ -73,7 +69,6 @@ describe('transformServiceMapResponses', () => { it('collapses external destinations based on span.destination.resource.name', () => { const response: ServiceMapResponse = { - anomalies: serviceAnomalies, services: [nodejsService, javaService], discoveredServices: [ { @@ -109,7 +104,6 @@ describe('transformServiceMapResponses', () => { it('picks the first span.type/subtype in an alphabetically sorted list', () => { const response: ServiceMapResponse = { - anomalies: serviceAnomalies, services: [javaService], discoveredServices: [], connections: [ @@ -148,7 +142,6 @@ describe('transformServiceMapResponses', () => { it('processes connections without a matching "service" aggregation', () => { const response: ServiceMapResponse = { - anomalies: serviceAnomalies, services: [javaService], discoveredServices: [], connections: [ diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index 53abf54cbcf31..835c00b8df239 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -17,12 +17,7 @@ import { ServiceConnectionNode, ExternalConnectionNode, } from '../../../common/service_map'; -import { - ConnectionsResponse, - ServicesResponse, - ServiceAnomalies, -} from './get_service_map'; -import { addAnomaliesDataToNodes } from './ml_helpers'; +import { ConnectionsResponse, ServicesResponse } from './get_service_map'; function getConnectionNodeId(node: ConnectionNode): string { if ('span.destination.service.resource' in node) { @@ -67,12 +62,11 @@ export function getServiceNodes(allNodes: ConnectionNode[]) { } export type ServiceMapResponse = ConnectionsResponse & { - anomalies: ServiceAnomalies; services: ServicesResponse; }; export function transformServiceMapResponses(response: ServiceMapResponse) { - const { anomalies, discoveredServices, services, connections } = response; + const { discoveredServices, services, connections } = response; const allNodes = getAllNodes(services, connections); const serviceNodes = getServiceNodes(allNodes); @@ -214,18 +208,10 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { return prev.concat(connection); }, []); - // Add anomlies data - const dedupedNodesWithAnomliesData = addAnomaliesDataToNodes( - dedupedNodes, - anomalies - ); - // Put everything together in elements, with everything in the "data" property - const elements = [...dedupedConnections, ...dedupedNodesWithAnomliesData].map( - (element) => ({ - data: element, - }) - ); + const elements = [...dedupedConnections, ...dedupedNodes].map((element) => ({ + data: element, + })); return { elements }; } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/fetcher.test.ts.snap deleted file mode 100644 index cf3fdac221b59..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/fetcher.test.ts.snap +++ /dev/null @@ -1,68 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`anomalyAggsFetcher when ES returns valid response should call client with correct query 1`] = ` -Array [ - Array [ - Object { - "body": Object { - "aggs": Object { - "ml_avg_response_times": Object { - "aggs": Object { - "anomaly_score": Object { - "max": Object { - "field": "anomaly_score", - }, - }, - "lower": Object { - "min": Object { - "field": "model_lower", - }, - }, - "upper": Object { - "max": Object { - "field": "model_upper", - }, - }, - }, - "date_histogram": Object { - "extended_bounds": Object { - "max": 200000, - "min": 90000, - }, - "field": "timestamp", - "fixed_interval": "myInterval", - "min_doc_count": 0, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "job_id": "myservicename-mytransactiontype-high_mean_response_time", - }, - }, - Object { - "exists": Object { - "field": "bucket_span", - }, - }, - Object { - "range": Object { - "timestamp": Object { - "format": "epoch_millis", - "gte": 90000, - "lte": 200000, - }, - }, - }, - ], - }, - }, - "size": 0, - }, - }, - ], -] -`; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/index.test.ts.snap deleted file mode 100644 index 971fa3b92cc83..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/index.test.ts.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`getAnomalySeries should match snapshot 1`] = ` -Object { - "anomalyBoundaries": Array [ - Object { - "x": 5000, - "y": 200, - "y0": 20, - }, - Object { - "x": 15000, - "y": 100, - "y0": 20, - }, - Object { - "x": 25000, - "y": 50, - "y0": 10, - }, - Object { - "x": 30000, - "y": 50, - "y0": 10, - }, - ], - "anomalyScore": Array [ - Object { - "x": 25000, - "x0": 15000, - }, - Object { - "x": 35000, - "x0": 25000, - }, - ], -} -`; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/transform.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/transform.test.ts.snap deleted file mode 100644 index 8cf471cb34ed2..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/__snapshots__/transform.test.ts.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`anomalySeriesTransform should match snapshot 1`] = ` -Object { - "anomalyBoundaries": Array [ - Object { - "x": 10000, - "y": 200, - "y0": 20, - }, - Object { - "x": 15000, - "y": 100, - "y0": 20, - }, - Object { - "x": 25000, - "y": 50, - "y0": 10, - }, - ], - "anomalyScore": Array [ - Object { - "x": 25000, - "x0": 15000, - }, - Object { - "x": 25000, - "x0": 25000, - }, - ], -} -`; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.test.ts deleted file mode 100644 index 313cf818a322d..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { anomalySeriesFetcher, ESResponse } from './fetcher'; - -describe('anomalyAggsFetcher', () => { - describe('when ES returns valid response', () => { - let response: ESResponse | undefined; - let clientSpy: jest.Mock; - - beforeEach(async () => { - clientSpy = jest.fn().mockReturnValue('ES Response'); - response = await anomalySeriesFetcher({ - serviceName: 'myServiceName', - transactionType: 'myTransactionType', - intervalString: 'myInterval', - mlBucketSize: 10, - setup: { - ml: { - mlSystem: { - mlAnomalySearch: clientSpy, - }, - } as any, - start: 100000, - end: 200000, - } as any, - }); - }); - - it('should call client with correct query', () => { - expect(clientSpy.mock.calls).toMatchSnapshot(); - }); - - it('should return correct response', () => { - expect(response).toBe('ES Response'); - }); - }); - - it('should swallow HTTP errors', () => { - const httpError = new Error('anomaly lookup failed') as any; - httpError.statusCode = 418; - const failedRequestSpy = jest.fn(() => Promise.reject(httpError)); - - return expect( - anomalySeriesFetcher({ - setup: { - ml: { - mlSystem: { - mlAnomalySearch: failedRequestSpy, - }, - } as any, - }, - } as any) - ).resolves.toEqual(undefined); - }); - - it('should throw other errors', () => { - const otherError = new Error('anomaly lookup ASPLODED') as any; - const failedRequestSpy = jest.fn(() => Promise.reject(otherError)); - - return expect( - anomalySeriesFetcher({ - setup: { - ml: { - mlSystem: { - mlAnomalySearch: failedRequestSpy, - }, - } as any, - }, - } as any) - ).rejects.toThrow(otherError); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts deleted file mode 100644 index 8ee078de7f3ce..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts +++ /dev/null @@ -1,90 +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 { getMlJobId } from '../../../../../common/ml_job_constants'; -import { PromiseReturnType } from '../../../../../../observability/typings/common'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; - -export type ESResponse = Exclude< - PromiseReturnType, - undefined ->; - -export async function anomalySeriesFetcher({ - serviceName, - transactionType, - intervalString, - mlBucketSize, - setup, -}: { - serviceName: string; - transactionType: string; - intervalString: string; - mlBucketSize: number; - setup: Setup & SetupTimeRange; -}) { - const { ml, start, end } = setup; - if (!ml) { - return; - } - - // move the start back with one bucket size, to ensure to get anomaly data in the beginning - // this is required because ML has a minimum bucket size (default is 900s) so if our buckets are smaller, we might have several null buckets in the beginning - const newStart = start - mlBucketSize * 1000; - const jobId = getMlJobId(serviceName, transactionType); - - const params = { - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { job_id: jobId } }, - { exists: { field: 'bucket_span' } }, - { - range: { - timestamp: { - gte: newStart, - lte: end, - format: 'epoch_millis', - }, - }, - }, - ], - }, - }, - aggs: { - ml_avg_response_times: { - date_histogram: { - field: 'timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { - min: newStart, - max: end, - }, - }, - aggs: { - anomaly_score: { max: { field: 'anomaly_score' } }, - lower: { min: { field: 'model_lower' } }, - upper: { max: { field: 'model_upper' } }, - }, - }, - }, - }, - }; - - try { - const response = await ml.mlSystem.mlAnomalySearch(params); - return response; - } catch (err) { - const isHttpError = 'statusCode' in err; - if (isHttpError) { - return; - } - throw err; - } -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts deleted file mode 100644 index d649bfb192739..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getMlJobId } from '../../../../../common/ml_job_constants'; -import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; - -interface IOptions { - serviceName: string; - transactionType: string; - setup: Setup & SetupTimeRange; -} - -interface ESResponse { - bucket_span: number; -} - -export async function getMlBucketSize({ - serviceName, - transactionType, - setup, -}: IOptions): Promise { - const { ml, start, end } = setup; - if (!ml) { - return 0; - } - const jobId = getMlJobId(serviceName, transactionType); - - const params = { - body: { - _source: 'bucket_span', - size: 1, - query: { - bool: { - filter: [ - { term: { job_id: jobId } }, - { exists: { field: 'bucket_span' } }, - { - range: { - timestamp: { - gte: start, - lte: end, - format: 'epoch_millis', - }, - }, - }, - ], - }, - }, - }, - }; - - try { - const resp = await ml.mlSystem.mlAnomalySearch(params); - return resp.hits.hits[0]?._source.bucket_span || 0; - } catch (err) { - const isHttpError = 'statusCode' in err; - if (isHttpError) { - return 0; - } - throw err; - } -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts deleted file mode 100644 index fb87f1b5707d1..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getAnomalySeries } from '.'; -import { mlAnomalyResponse } from './mock_responses/ml_anomaly_response'; -import { mlBucketSpanResponse } from './mock_responses/ml_bucket_span_response'; -import { PromiseReturnType } from '../../../../../../observability/typings/common'; -import { APMConfig } from '../../../..'; - -describe('getAnomalySeries', () => { - let avgAnomalies: PromiseReturnType; - beforeEach(async () => { - const clientSpy = jest - .fn() - .mockResolvedValueOnce(mlBucketSpanResponse) - .mockResolvedValueOnce(mlAnomalyResponse); - - avgAnomalies = await getAnomalySeries({ - serviceName: 'myServiceName', - transactionType: 'myTransactionType', - transactionName: undefined, - timeSeriesDates: [100, 100000], - setup: { - start: 0, - end: 500000, - client: { search: () => {} } as any, - internalClient: { search: () => {} } as any, - config: new Proxy( - {}, - { - get: () => 'myIndex', - } - ) as APMConfig, - uiFiltersES: [], - indices: { - 'apm_oss.sourcemapIndices': 'myIndex', - 'apm_oss.errorIndices': 'myIndex', - 'apm_oss.onboardingIndices': 'myIndex', - 'apm_oss.spanIndices': 'myIndex', - 'apm_oss.transactionIndices': 'myIndex', - 'apm_oss.metricsIndices': 'myIndex', - apmAgentConfigurationIndex: 'myIndex', - apmCustomLinkIndex: 'myIndex', - }, - dynamicIndexPattern: null as any, - ml: { - mlSystem: { - mlAnomalySearch: clientSpy, - mlCapabilities: async () => ({ isPlatinumOrTrialLicense: true }), - }, - } as any, - }, - }); - }); - - it('should remove buckets lower than threshold and outside date range from anomalyScore', () => { - expect(avgAnomalies!.anomalyScore).toEqual([ - { x0: 15000, x: 25000 }, - { x0: 25000, x: 35000 }, - ]); - }); - - it('should remove buckets outside date range from anomalyBoundaries', () => { - expect( - avgAnomalies!.anomalyBoundaries!.filter( - (bucket) => bucket.x < 100 || bucket.x > 100000 - ).length - ).toBe(0); - }); - - it('should remove buckets with null from anomalyBoundaries', () => { - expect( - avgAnomalies!.anomalyBoundaries!.filter((p) => p.y === null).length - ).toBe(0); - }); - - it('should match snapshot', async () => { - expect(avgAnomalies).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts index 6f44cfa1df9f0..b2d11f2ffe19a 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts @@ -4,15 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getBucketSize } from '../../../helpers/get_bucket_size'; import { Setup, SetupTimeRange, SetupUIFilters, } from '../../../helpers/setup_request'; -import { anomalySeriesFetcher } from './fetcher'; -import { getMlBucketSize } from './get_ml_bucket_size'; -import { anomalySeriesTransform } from './transform'; +import { Coordinate, RectCoordinate } from '../../../../../typings/timeseries'; + +interface AnomalyTimeseries { + anomalyBoundaries: Coordinate[]; + anomalyScore: RectCoordinate[]; +} export async function getAnomalySeries({ serviceName, @@ -26,7 +28,7 @@ export async function getAnomalySeries({ transactionName: string | undefined; timeSeriesDates: number[]; setup: Setup & SetupTimeRange & SetupUIFilters; -}) { +}): Promise { // don't fetch anomalies for transaction details page if (transactionName) { return; @@ -53,29 +55,6 @@ export async function getAnomalySeries({ return; } - const mlBucketSize = await getMlBucketSize({ - serviceName, - transactionType, - setup, - }); - - const { start, end } = setup; - const { intervalString, bucketSize } = getBucketSize(start, end, 'auto'); - - const esResponse = await anomalySeriesFetcher({ - serviceName, - transactionType, - intervalString, - mlBucketSize, - setup, - }); - - return esResponse - ? anomalySeriesTransform( - esResponse, - mlBucketSize, - bucketSize, - timeSeriesDates - ) - : undefined; + // TODO [APM ML] return a series of anomaly scores, upper & lower bounds for the given timeSeriesDates + return; } diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_anomaly_response.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_anomaly_response.ts deleted file mode 100644 index 523161ec10275..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_anomaly_response.ts +++ /dev/null @@ -1,127 +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 { ESResponse } from '../fetcher'; - -export const mlAnomalyResponse: ESResponse = ({ - took: 3, - timed_out: false, - _shards: { - total: 5, - successful: 5, - skipped: 0, - failed: 0, - }, - hits: { - total: 10, - max_score: 0, - hits: [], - }, - aggregations: { - ml_avg_response_times: { - buckets: [ - { - key_as_string: '2018-07-02T09:16:40.000Z', - key: 0, - doc_count: 0, - anomaly_score: { - value: null, - }, - upper: { - value: 200, - }, - lower: { - value: 20, - }, - }, - { - key_as_string: '2018-07-02T09:25:00.000Z', - key: 5000, - doc_count: 4, - anomaly_score: { - value: null, - }, - upper: { - value: null, - }, - lower: { - value: null, - }, - }, - { - key_as_string: '2018-07-02T09:33:20.000Z', - key: 10000, - doc_count: 0, - anomaly_score: { - value: null, - }, - upper: { - value: null, - }, - lower: { - value: null, - }, - }, - { - key_as_string: '2018-07-02T09:41:40.000Z', - key: 15000, - doc_count: 2, - anomaly_score: { - value: 90, - }, - upper: { - value: 100, - }, - lower: { - value: 20, - }, - }, - { - key_as_string: '2018-07-02T09:50:00.000Z', - key: 20000, - doc_count: 0, - anomaly_score: { - value: null, - }, - upper: { - value: null, - }, - lower: { - value: null, - }, - }, - { - key_as_string: '2018-07-02T09:58:20.000Z', - key: 25000, - doc_count: 2, - anomaly_score: { - value: 100, - }, - upper: { - value: 50, - }, - lower: { - value: 10, - }, - }, - { - key_as_string: '2018-07-02T10:15:00.000Z', - key: 30000, - doc_count: 2, - anomaly_score: { - value: 0, - }, - upper: { - value: null, - }, - lower: { - value: null, - }, - }, - ], - }, - }, -} as unknown) as ESResponse; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_bucket_span_response.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_bucket_span_response.ts deleted file mode 100644 index 148d4a7ffcf6f..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock_responses/ml_bucket_span_response.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const mlBucketSpanResponse = { - took: 1, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: 192, - max_score: 1.0, - hits: [ - { - _index: '.ml-anomalies-shared', - _type: 'doc', - _id: - 'opbeans-go-request-high_mean_response_time_model_plot_1542636000000_900_0_29791_0', - _score: 1.0, - _source: { - bucket_span: 10, - }, - }, - ], - }, -}; diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts deleted file mode 100644 index eb94c83e92576..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.test.ts +++ /dev/null @@ -1,303 +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 { ESResponse } from './fetcher'; -import { mlAnomalyResponse } from './mock_responses/ml_anomaly_response'; -import { anomalySeriesTransform, replaceFirstAndLastBucket } from './transform'; - -describe('anomalySeriesTransform', () => { - it('should match snapshot', () => { - const getMlBucketSize = 10; - const bucketSize = 5; - const timeSeriesDates = [10000, 25000]; - const anomalySeries = anomalySeriesTransform( - mlAnomalyResponse, - getMlBucketSize, - bucketSize, - timeSeriesDates - ); - expect(anomalySeries).toMatchSnapshot(); - }); - - describe('anomalyScoreSeries', () => { - it('should only returns bucket within range and above threshold', () => { - const esResponse = getESResponse([ - { - key: 0, - anomaly_score: { value: 90 }, - }, - { - key: 5000, - anomaly_score: { value: 0 }, - }, - { - key: 10000, - anomaly_score: { value: 90 }, - }, - { - key: 15000, - anomaly_score: { value: 0 }, - }, - { - key: 20000, - anomaly_score: { value: 90 }, - }, - ]); - - const getMlBucketSize = 5; - const bucketSize = 5; - const timeSeriesDates = [5000, 15000]; - const anomalySeries = anomalySeriesTransform( - esResponse, - getMlBucketSize, - bucketSize, - timeSeriesDates - ); - - const buckets = anomalySeries!.anomalyScore; - expect(buckets).toEqual([{ x0: 10000, x: 15000 }]); - }); - - it('should decrease the x-value to avoid going beyond last date', () => { - const esResponse = getESResponse([ - { - key: 0, - anomaly_score: { value: 0 }, - }, - { - key: 5000, - anomaly_score: { value: 90 }, - }, - ]); - - const getMlBucketSize = 10; - const bucketSize = 5; - const timeSeriesDates = [0, 10000]; - const anomalySeries = anomalySeriesTransform( - esResponse, - getMlBucketSize, - bucketSize, - timeSeriesDates - ); - - const buckets = anomalySeries!.anomalyScore; - expect(buckets).toEqual([{ x0: 5000, x: 10000 }]); - }); - }); - - describe('anomalyBoundariesSeries', () => { - it('should trim buckets to time range', () => { - const esResponse = getESResponse([ - { - key: 0, - upper: { value: 15 }, - lower: { value: 10 }, - }, - { - key: 5000, - upper: { value: 25 }, - lower: { value: 20 }, - }, - { - key: 10000, - upper: { value: 35 }, - lower: { value: 30 }, - }, - { - key: 15000, - upper: { value: 45 }, - lower: { value: 40 }, - }, - ]); - - const mlBucketSize = 10; - const bucketSize = 5; - const timeSeriesDates = [5000, 10000]; - const anomalySeries = anomalySeriesTransform( - esResponse, - mlBucketSize, - bucketSize, - timeSeriesDates - ); - - const buckets = anomalySeries!.anomalyBoundaries; - expect(buckets).toEqual([ - { x: 5000, y: 25, y0: 20 }, - { x: 10000, y: 35, y0: 30 }, - ]); - }); - - it('should replace first bucket in range', () => { - const esResponse = getESResponse([ - { - key: 0, - anomaly_score: { value: 0 }, - upper: { value: 15 }, - lower: { value: 10 }, - }, - { - key: 5000, - anomaly_score: { value: 0 }, - upper: { value: null }, - lower: { value: null }, - }, - { - key: 10000, - anomaly_score: { value: 0 }, - upper: { value: 25 }, - lower: { value: 20 }, - }, - ]); - - const getMlBucketSize = 10; - const bucketSize = 5; - const timeSeriesDates = [5000, 10000]; - const anomalySeries = anomalySeriesTransform( - esResponse, - getMlBucketSize, - bucketSize, - timeSeriesDates - ); - - const buckets = anomalySeries!.anomalyBoundaries; - expect(buckets).toEqual([ - { x: 5000, y: 15, y0: 10 }, - { x: 10000, y: 25, y0: 20 }, - ]); - }); - - it('should replace last bucket in range', () => { - const esResponse = getESResponse([ - { - key: 0, - anomaly_score: { value: 0 }, - upper: { value: 15 }, - lower: { value: 10 }, - }, - { - key: 5000, - anomaly_score: { value: 0 }, - upper: { value: null }, - lower: { value: null }, - }, - { - key: 10000, - anomaly_score: { value: 0 }, - upper: { value: null }, - lower: { value: null }, - }, - ]); - - const getMlBucketSize = 10; - const bucketSize = 5; - const timeSeriesDates = [5000, 10000]; - const anomalySeries = anomalySeriesTransform( - esResponse, - getMlBucketSize, - bucketSize, - timeSeriesDates - ); - - const buckets = anomalySeries!.anomalyBoundaries; - expect(buckets).toEqual([ - { x: 5000, y: 15, y0: 10 }, - { x: 10000, y: 15, y0: 10 }, - ]); - }); - }); -}); - -describe('replaceFirstAndLastBucket', () => { - it('should extend the first bucket', () => { - const buckets = [ - { - x: 0, - lower: 10, - upper: 20, - }, - { - x: 5, - lower: null, - upper: null, - }, - { - x: 10, - lower: null, - upper: null, - }, - { - x: 15, - lower: 30, - upper: 40, - }, - ]; - - const timeSeriesDates = [10, 15]; - expect(replaceFirstAndLastBucket(buckets as any, timeSeriesDates)).toEqual([ - { x: 10, lower: 10, upper: 20 }, - { x: 15, lower: 30, upper: 40 }, - ]); - }); - - it('should extend the last bucket', () => { - const buckets = [ - { - x: 10, - lower: 30, - upper: 40, - }, - { - x: 15, - lower: null, - upper: null, - }, - { - x: 20, - lower: null, - upper: null, - }, - ] as any; - - const timeSeriesDates = [10, 15, 20]; - expect(replaceFirstAndLastBucket(buckets, timeSeriesDates)).toEqual([ - { x: 10, lower: 30, upper: 40 }, - { x: 15, lower: null, upper: null }, - { x: 20, lower: 30, upper: 40 }, - ]); - }); -}); - -function getESResponse(buckets: any): ESResponse { - return ({ - took: 3, - timed_out: false, - _shards: { - total: 5, - successful: 5, - skipped: 0, - failed: 0, - }, - hits: { - total: 10, - max_score: 0, - hits: [], - }, - aggregations: { - ml_avg_response_times: { - buckets: buckets.map((bucket: any) => { - return { - ...bucket, - lower: { value: bucket?.lower?.value || null }, - upper: { value: bucket?.upper?.value || null }, - anomaly_score: { - value: bucket?.anomaly_score?.value || null, - }, - }; - }), - }, - }, - } as unknown) as ESResponse; -} diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts deleted file mode 100644 index 454a6add3e256..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/transform.ts +++ /dev/null @@ -1,126 +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 { first, last } from 'lodash'; -import { Coordinate, RectCoordinate } from '../../../../../typings/timeseries'; -import { ESResponse } from './fetcher'; - -type IBucket = ReturnType; -function getBucket( - bucket: Required< - ESResponse - >['aggregations']['ml_avg_response_times']['buckets'][0] -) { - return { - x: bucket.key, - anomalyScore: bucket.anomaly_score.value, - lower: bucket.lower.value, - upper: bucket.upper.value, - }; -} - -export type AnomalyTimeSeriesResponse = ReturnType< - typeof anomalySeriesTransform ->; -export function anomalySeriesTransform( - response: ESResponse, - mlBucketSize: number, - bucketSize: number, - timeSeriesDates: number[] -) { - const buckets = - response.aggregations?.ml_avg_response_times.buckets.map(getBucket) || []; - - const bucketSizeInMillis = Math.max(bucketSize, mlBucketSize) * 1000; - - return { - anomalyScore: getAnomalyScoreDataPoints( - buckets, - timeSeriesDates, - bucketSizeInMillis - ), - anomalyBoundaries: getAnomalyBoundaryDataPoints(buckets, timeSeriesDates), - }; -} - -export function getAnomalyScoreDataPoints( - buckets: IBucket[], - timeSeriesDates: number[], - bucketSizeInMillis: number -): RectCoordinate[] { - const ANOMALY_THRESHOLD = 75; - const firstDate = first(timeSeriesDates); - const lastDate = last(timeSeriesDates); - - return buckets - .filter( - (bucket) => - bucket.anomalyScore !== null && bucket.anomalyScore > ANOMALY_THRESHOLD - ) - .filter(isInDateRange(firstDate, lastDate)) - .map((bucket) => { - return { - x0: bucket.x, - x: Math.min(bucket.x + bucketSizeInMillis, lastDate), // don't go beyond last date - }; - }); -} - -export function getAnomalyBoundaryDataPoints( - buckets: IBucket[], - timeSeriesDates: number[] -): Coordinate[] { - return replaceFirstAndLastBucket(buckets, timeSeriesDates) - .filter((bucket) => bucket.lower !== null) - .map((bucket) => { - return { - x: bucket.x, - y0: bucket.lower, - y: bucket.upper, - }; - }); -} - -export function replaceFirstAndLastBucket( - buckets: IBucket[], - timeSeriesDates: number[] -) { - const firstDate = first(timeSeriesDates); - const lastDate = last(timeSeriesDates); - - const preBucketWithValue = buckets - .filter((p) => p.x <= firstDate) - .reverse() - .find((p) => p.lower !== null); - - const bucketsInRange = buckets.filter(isInDateRange(firstDate, lastDate)); - - // replace first bucket if it is null - const firstBucket = first(bucketsInRange); - if (preBucketWithValue && firstBucket && firstBucket.lower === null) { - firstBucket.lower = preBucketWithValue.lower; - firstBucket.upper = preBucketWithValue.upper; - } - - const lastBucketWithValue = [...buckets] - .reverse() - .find((p) => p.lower !== null); - - // replace last bucket if it is null - const lastBucket = last(bucketsInRange); - if (lastBucketWithValue && lastBucket && lastBucket.lower === null) { - lastBucket.lower = lastBucketWithValue.lower; - lastBucket.upper = lastBucketWithValue.upper; - } - - return bucketsInRange; -} - -// anomaly time series contain one or more buckets extra in the beginning -// these extra buckets should be removed -function isInDateRange(firstDate: number, lastDate: number) { - return (p: IBucket) => p.x >= firstDate && p.x <= lastDate; -} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c8a8027fb8bb4..bda29c0135ebe 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4300,21 +4300,6 @@ "xpack.apm.serviceDetails.alertsMenu.errorRate": "エラー率", "xpack.apm.serviceDetails.alertsMenu.transactionDuration": "トランザクション期間", "xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts": "アクティブアラートを表示", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription": "現在 {serviceName} ({transactionType}) の実行中のジョブがあります。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText": "既存のジョブを表示", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsTitle": "ジョブが既に存在します", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText": "トランザクション時間のグラフ", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel": "ジョブを作成", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle": "異常検知を有効にする", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText": "現在 {serviceName} ({transactionType}) の分析を実行中です。応答時間グラフに結果が追加されるまで少し時間がかかる場合があります。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText": "ジョブを表示", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationTitle": "ジョブが作成されました", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationText": "現在のライセンスでは機械学習ジョブの作成が許可されていないか、ジョブが既に存在する可能性があります。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle": "ジョブの作成に失敗", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription": "ジョブはそれぞれのサービス + トランザクションタイプの組み合わせに対して作成できます。ジョブの作成後、{mlJobsPageLink} で管理と詳細の確認ができます。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText": "機械学習ジョブの管理ページ", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.noteText": "注:ジョブが結果の計算を開始するまでに少し時間がかかる場合があります。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.selectTransactionTypeLabel": "このジョブのトランザクションタイプを選択してください", "xpack.apm.serviceDetails.enableErrorReportsPanel.actionsDescription": "レポートはメールで送信するか Slack チャンネルに投稿できます。各レポートにはオカランス別のトップ 10 のエラーが含まれます。", "xpack.apm.serviceDetails.enableErrorReportsPanel.actionsTitle": "アクション", "xpack.apm.serviceDetails.enableErrorReportsPanel.conditionTitle": "コンディション", @@ -4350,8 +4335,6 @@ "xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationText": "ユーザーにウォッチ作成のパーミッションがあることを確認してください。", "xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationTitle": "ウォッチの作成に失敗", "xpack.apm.serviceDetails.errorsTabLabel": "エラー", - "xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonLabel": "ML 異常検知を有効にする", - "xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonTooltip": "このサービスの機械学習ジョブをセットアップします", "xpack.apm.serviceDetails.integrationsMenu.enableWatcherErrorReportsButtonLabel": "ウォッチエラーレポートを有効にする", "xpack.apm.serviceDetails.integrationsMenu.integrationsButtonLabel": "統合", "xpack.apm.serviceDetails.integrationsMenu.viewWatchesButtonLabel": "既存のウォッチを表示", @@ -4361,9 +4344,6 @@ "xpack.apm.serviceDetails.metricsTabLabel": "メトリック", "xpack.apm.serviceDetails.nodesTabLabel": "JVM", "xpack.apm.serviceDetails.transactionsTabLabel": "トランザクション", - "xpack.apm.serviceMap.anomalyDetectionPopoverLink": "異常を表示", - "xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric": "スコア(最大)", - "xpack.apm.serviceMap.anomalyDetectionPopoverTitle": "異常検知", "xpack.apm.serviceMap.avgCpuUsagePopoverMetric": "CPU使用状況 (平均)", "xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric": "1分あたりのエラー(平均)", "xpack.apm.serviceMap.avgMemoryUsagePopoverMetric": "メモリー使用状況(平均)", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 04eb5a0d4c172..3f53130d00e75 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4303,21 +4303,6 @@ "xpack.apm.serviceDetails.alertsMenu.errorRate": "错误率", "xpack.apm.serviceDetails.alertsMenu.transactionDuration": "事务持续时间", "xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts": "查看活动的告警", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription": "当前有 {serviceName}({transactionType})的作业正在运行。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsDescription.viewJobLinkText": "查看现有作业", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.callout.jobExistsTitle": "作业已存在", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createMLJobDescription.transactionDurationGraphText": "事务持续时间图表", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.createNewJobButtonLabel": "创建作业", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.enableAnomalyDetectionTitle": "启用异常检测", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText": "现在正在运行对 {serviceName}({transactionType})的分析。可能要花费点时间,才会将结果添加响应时间图表。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText": "查看作业", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationTitle": "作业已成功创建", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationText": "您当前的许可可能不允许创建 Machine Learning 作业,或者此作业可能已存在。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle": "作业创建失败", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription": "可以创建每个服务 + 事务类型组合的作业。创建作业后,可以在 {mlJobsPageLink}中管理作业以及查看更多详细信息。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.mlJobsPageLinkText": "Machine Learning 作业管理页面", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.manageMLJobDescription.noteText": "注意:可能要过几分钟后,作业才会开始计算结果。", - "xpack.apm.serviceDetails.enableAnomalyDetectionPanel.selectTransactionTypeLabel": "为此作业选择事务类型", "xpack.apm.serviceDetails.enableErrorReportsPanel.actionsDescription": "可以通过电子邮件发送报告或将报告发布到 Slack 频道。每个报告将包括按发生次数排序的前 10 个错误。", "xpack.apm.serviceDetails.enableErrorReportsPanel.actionsTitle": "操作", "xpack.apm.serviceDetails.enableErrorReportsPanel.conditionTitle": "条件", @@ -4353,8 +4338,6 @@ "xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationText": "确保您的用户有权创建监视。", "xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationTitle": "监视创建失败", "xpack.apm.serviceDetails.errorsTabLabel": "错误", - "xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonLabel": "启用 ML 异常检测", - "xpack.apm.serviceDetails.integrationsMenu.enableMLAnomalyDetectionButtonTooltip": "为此服务设置 Machine Learning 作业", "xpack.apm.serviceDetails.integrationsMenu.enableWatcherErrorReportsButtonLabel": "启用 Watcher 错误报告", "xpack.apm.serviceDetails.integrationsMenu.integrationsButtonLabel": "集成", "xpack.apm.serviceDetails.integrationsMenu.viewWatchesButtonLabel": "查看现有监视", @@ -4364,9 +4347,6 @@ "xpack.apm.serviceDetails.metricsTabLabel": "指标", "xpack.apm.serviceDetails.nodesTabLabel": "JVM", "xpack.apm.serviceDetails.transactionsTabLabel": "事务", - "xpack.apm.serviceMap.anomalyDetectionPopoverLink": "查看异常", - "xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric": "分数(最大)", - "xpack.apm.serviceMap.anomalyDetectionPopoverTitle": "异常检测", "xpack.apm.serviceMap.avgCpuUsagePopoverMetric": "CPU 使用(平均)", "xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric": "每分钟错误数(平均)", "xpack.apm.serviceMap.avgMemoryUsagePopoverMetric": "内存使用(平均)", From 000c14ed19c244fcbcf1a67e3007d82fd4e14d5b Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Wed, 24 Jun 2020 19:19:31 -0400 Subject: [PATCH 14/82] [7.x] Convert Positionable, RenderToDom and RenderWithFn to functional/hooks/no recompose. (#68202) (#69855) Co-authored-by: Elastic Machine # Conflicts: # x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.js --- .../element_content/element_content.js | 12 +- .../element_wrapper/element_wrapper.js | 8 +- .../components/element_wrapper/index.js | 4 +- .../element_wrapper/lib/handlers.js | 60 --------- .../positionable/{index.js => index.ts} | 5 +- .../components/positionable/positionable.js | 42 ------- .../components/positionable/positionable.tsx | 48 +++++++ .../public/components/render_to_dom/index.js | 12 -- .../handlers.js => render_to_dom/index.ts} | 14 +-- .../components/render_to_dom/render_to_dom.js | 40 ------ .../render_to_dom/render_to_dom.tsx | 27 ++++ .../public/components/render_with_fn/index.js | 30 ----- .../public/components/render_with_fn/index.ts | 7 ++ .../render_with_fn/render_with_fn.tsx | 117 ++++++++++++++++++ .../canvas/public/lib/create_handlers.ts | 96 ++++++++++++++ .../components/rendered_element.tsx | 14 +-- x-pack/plugins/canvas/types/renderers.ts | 28 +++-- 17 files changed, 331 insertions(+), 233 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js rename x-pack/plugins/canvas/public/components/positionable/{index.js => index.ts} (63%) delete mode 100644 x-pack/plugins/canvas/public/components/positionable/positionable.js create mode 100644 x-pack/plugins/canvas/public/components/positionable/positionable.tsx delete mode 100644 x-pack/plugins/canvas/public/components/render_to_dom/index.js rename x-pack/plugins/canvas/public/components/{render_with_fn/lib/handlers.js => render_to_dom/index.ts} (61%) delete mode 100644 x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js create mode 100644 x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx delete mode 100644 x-pack/plugins/canvas/public/components/render_with_fn/index.js create mode 100644 x-pack/plugins/canvas/public/components/render_with_fn/index.ts create mode 100644 x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx create mode 100644 x-pack/plugins/canvas/public/lib/create_handlers.ts diff --git a/x-pack/plugins/canvas/public/components/element_content/element_content.js b/x-pack/plugins/canvas/public/components/element_content/element_content.js index 114a457d167e7..e2c1a61c348d1 100644 --- a/x-pack/plugins/canvas/public/components/element_content/element_content.js +++ b/x-pack/plugins/canvas/public/components/element_content/element_content.js @@ -12,6 +12,7 @@ import { getType } from '@kbn/interpreter/common'; import { Loading } from '../loading'; import { RenderWithFn } from '../render_with_fn'; import { ElementShareContainer } from '../element_share_container'; +import { assignHandlers } from '../../lib/create_handlers'; import { InvalidExpression } from './invalid_expression'; import { InvalidElementType } from './invalid_element_type'; @@ -46,7 +47,7 @@ const branches = [ export const ElementContent = compose( pure, ...branches -)(({ renderable, renderFunction, size, handlers }) => { +)(({ renderable, renderFunction, width, height, handlers }) => { const { getFilter, setFilter, @@ -62,7 +63,7 @@ export const ElementContent = compose(
diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js b/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js index 845fc5927d839..de7748413b718 100644 --- a/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js +++ b/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js @@ -14,7 +14,13 @@ export const ElementWrapper = (props) => { return ( - + ); }; diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/index.js b/x-pack/plugins/canvas/public/components/element_wrapper/index.js index 390c349ab2ee6..6fc582bfee444 100644 --- a/x-pack/plugins/canvas/public/components/element_wrapper/index.js +++ b/x-pack/plugins/canvas/public/components/element_wrapper/index.js @@ -10,12 +10,12 @@ import { compose, withPropsOnChange, mapProps } from 'recompose'; import isEqual from 'react-fast-compare'; import { getResolvedArgs, getSelectedPage } from '../../state/selectors/workpad'; import { getState, getValue } from '../../lib/resolved_arg'; +import { createDispatchedHandlerFactory } from '../../lib/create_handlers'; import { ElementWrapper as Component } from './element_wrapper'; -import { createHandlers as createHandlersWithDispatch } from './lib/handlers'; function selectorFactory(dispatch) { let result = {}; - const createHandlers = createHandlersWithDispatch(dispatch); + const createHandlers = createDispatchedHandlerFactory(dispatch); return (nextState, nextOwnProps) => { const { element, ...restOwnProps } = nextOwnProps; diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js b/x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js deleted file mode 100644 index 33e8eacd902dd..0000000000000 --- a/x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js +++ /dev/null @@ -1,60 +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 { isEqual } from 'lodash'; -import { setFilter } from '../../../state/actions/elements'; -import { - updateEmbeddableExpression, - fetchEmbeddableRenderable, -} from '../../../state/actions/embeddable'; - -export const createHandlers = (dispatch) => { - let isComplete = false; - let oldElement; - let completeFn = () => {}; - - return (element) => { - // reset isComplete when element changes - if (!isEqual(oldElement, element)) { - isComplete = false; - oldElement = element; - } - - return { - setFilter(text) { - dispatch(setFilter(text, element.id, true)); - }, - - getFilter() { - return element.filter; - }, - - onComplete(fn) { - completeFn = fn; - }, - - getElementId: () => element.id, - - onEmbeddableInputChange(embeddableExpression) { - dispatch(updateEmbeddableExpression({ elementId: element.id, embeddableExpression })); - }, - - onEmbeddableDestroyed() { - dispatch(fetchEmbeddableRenderable(element.id)); - }, - - done() { - // don't emit if the element is already done - if (isComplete) { - return; - } - - isComplete = true; - completeFn(); - }, - }; - }; -}; diff --git a/x-pack/plugins/canvas/public/components/positionable/index.js b/x-pack/plugins/canvas/public/components/positionable/index.ts similarity index 63% rename from x-pack/plugins/canvas/public/components/positionable/index.js rename to x-pack/plugins/canvas/public/components/positionable/index.ts index e5c3c32acb024..964e2ee41df75 100644 --- a/x-pack/plugins/canvas/public/components/positionable/index.js +++ b/x-pack/plugins/canvas/public/components/positionable/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pure } from 'recompose'; -import { Positionable as Component } from './positionable'; - -export const Positionable = pure(Component); +export { Positionable } from './positionable'; diff --git a/x-pack/plugins/canvas/public/components/positionable/positionable.js b/x-pack/plugins/canvas/public/components/positionable/positionable.js deleted file mode 100644 index 9898f50cbb0f0..0000000000000 --- a/x-pack/plugins/canvas/public/components/positionable/positionable.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { matrixToCSS } from '../../lib/dom'; - -export const Positionable = ({ children, transformMatrix, width, height }) => { - // Throw if there is more than one child - React.Children.only(children); - // This could probably be made nicer by having just one child - const wrappedChildren = React.Children.map(children, (child) => { - const newStyle = { - width, - height, - marginLeft: -width / 2, - marginTop: -height / 2, - position: 'absolute', - transform: matrixToCSS(transformMatrix.map((n, i) => (i < 12 ? n : Math.round(n)))), - }; - - const stepChild = React.cloneElement(child, { size: { width, height } }); - return ( -
- {stepChild} -
- ); - }); - - return wrappedChildren; -}; - -Positionable.propTypes = { - onChange: PropTypes.func, - children: PropTypes.element.isRequired, - transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, -}; diff --git a/x-pack/plugins/canvas/public/components/positionable/positionable.tsx b/x-pack/plugins/canvas/public/components/positionable/positionable.tsx new file mode 100644 index 0000000000000..3344398b00198 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/positionable/positionable.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, ReactElement, CSSProperties } from 'react'; +import PropTypes from 'prop-types'; +import { matrixToCSS } from '../../lib/dom'; +import { TransformMatrix3d } from '../../lib/aeroelastic'; + +interface Props { + children: ReactElement; + transformMatrix: TransformMatrix3d; + height: number; + width: number; +} + +export const Positionable: FC = ({ children, transformMatrix, width, height }) => { + // Throw if there is more than one child + const childNode = React.Children.only(children); + + const matrix = (transformMatrix.map((n, i) => + i < 12 ? n : Math.round(n) + ) as any) as TransformMatrix3d; + + const newStyle: CSSProperties = { + width, + height, + marginLeft: -width / 2, + marginTop: -height / 2, + position: 'absolute', + transform: matrixToCSS(matrix), + }; + + return ( +
+ {childNode} +
+ ); +}; + +Positionable.propTypes = { + children: PropTypes.element.isRequired, + transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/index.js b/x-pack/plugins/canvas/public/components/render_to_dom/index.js deleted file mode 100644 index e8a3f8cd8c93b..0000000000000 --- a/x-pack/plugins/canvas/public/components/render_to_dom/index.js +++ /dev/null @@ -1,12 +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 { compose, withState } from 'recompose'; -import { RenderToDom as Component } from './render_to_dom'; - -export const RenderToDom = compose( - withState('domNode', 'setDomNode') // Still don't like this, seems to be the only way todo it. -)(Component); diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/lib/handlers.js b/x-pack/plugins/canvas/public/components/render_to_dom/index.ts similarity index 61% rename from x-pack/plugins/canvas/public/components/render_with_fn/lib/handlers.js rename to x-pack/plugins/canvas/public/components/render_to_dom/index.ts index 9e5032efa97e2..43a5dad059c95 100644 --- a/x-pack/plugins/canvas/public/components/render_with_fn/lib/handlers.js +++ b/x-pack/plugins/canvas/public/components/render_to_dom/index.ts @@ -4,16 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export class ElementHandlers { - resize() {} - - destroy() {} - - onResize(fn) { - this.resize = fn; - } - - onDestroy(fn) { - this.destroy = fn; - } -} +export { RenderToDom } from './render_to_dom'; diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js b/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js deleted file mode 100644 index db393a8dde4f9..0000000000000 --- a/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; - -export class RenderToDom extends React.Component { - static propTypes = { - domNode: PropTypes.object, - setDomNode: PropTypes.func.isRequired, - render: PropTypes.func.isRequired, - style: PropTypes.object, - }; - - shouldComponentUpdate(nextProps) { - return this.props.domNode !== nextProps.domNode; - } - - componentDidUpdate() { - // Calls render function once we have the reference to the DOM element to render into - if (this.props.domNode) { - this.props.render(this.props.domNode); - } - } - - render() { - const { domNode, setDomNode, style } = this.props; - const linkRef = (refNode) => { - if (!domNode && refNode) { - // Initialize the domNode property. This should only happen once, even if config changes. - setDomNode(refNode); - } - }; - - return
; - } -} diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx b/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx new file mode 100644 index 0000000000000..a37c0fc096e57 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, FC } from 'react'; +import CSS from 'csstype'; + +interface Props { + render: (element: HTMLElement) => void; + style?: CSS.Properties; +} + +export const RenderToDom: FC = ({ render, style }) => { + // https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node + const ref = useCallback( + (node: HTMLDivElement) => { + if (node !== null) { + render(node); + } + }, + [render] + ); + + return
; +}; diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/index.js b/x-pack/plugins/canvas/public/components/render_with_fn/index.js deleted file mode 100644 index 37c49624a3940..0000000000000 --- a/x-pack/plugins/canvas/public/components/render_with_fn/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { compose, withProps, withPropsOnChange } from 'recompose'; -import PropTypes from 'prop-types'; -import isEqual from 'react-fast-compare'; -import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { RenderWithFn as Component } from './render_with_fn'; -import { ElementHandlers } from './lib/handlers'; - -export const RenderWithFn = compose( - withPropsOnChange( - // rebuild elementHandlers when handlers object changes - (props, nextProps) => !isEqual(props.handlers, nextProps.handlers), - ({ handlers }) => ({ - handlers: Object.assign(new ElementHandlers(), handlers), - }) - ), - withKibana, - withProps((props) => ({ - onError: props.kibana.services.canvas.notify.error, - })) -)(Component); - -RenderWithFn.propTypes = { - handlers: PropTypes.object, -}; diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/index.ts b/x-pack/plugins/canvas/public/components/render_with_fn/index.ts new file mode 100644 index 0000000000000..4bfef734d34f4 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/render_with_fn/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 { RenderWithFn } from './render_with_fn'; diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx new file mode 100644 index 0000000000000..bc51128cf0c87 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect, useRef, FC, useCallback } from 'react'; +import { useDebounce } from 'react-use'; + +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { RenderToDom } from '../render_to_dom'; +import { ErrorStrings } from '../../../i18n'; +import { RendererHandlers } from '../../../types'; + +const { RenderWithFn: strings } = ErrorStrings; + +interface Props { + name: string; + renderFn: ( + domNode: HTMLElement, + config: Record, + handlers: RendererHandlers + ) => void | Promise; + reuseNode: boolean; + handlers: RendererHandlers; + config: Record; + height: number; + width: number; +} + +const style = { height: '100%', width: '100%' }; + +export const RenderWithFn: FC = ({ + name: functionName, + renderFn, + reuseNode = false, + handlers: incomingHandlers, + config, + width, + height, +}) => { + const { services } = useKibana(); + const onError = services.canvas.notify.error; + + const [domNode, setDomNode] = useState(null); + + // Tells us if the component is attempting to re-render into a previously-populated render target. + const firstRender = useRef(true); + // A reference to the node appended to the provided DOM node which is created and optionally replaced. + const renderTarget = useRef(null); + // A reference to the handlers, as the renderFn may mutate them, (via onXYZ functions) + const handlers = useRef(incomingHandlers); + + // Reset the render target, the node appended to the DOM node provided by RenderToDOM. + const resetRenderTarget = useCallback(() => { + if (!domNode) { + return; + } + + if (!firstRender) { + handlers.current.destroy(); + } + + while (domNode.firstChild) { + domNode.removeChild(domNode.firstChild); + } + + const div = document.createElement('div'); + div.style.width = '100%'; + div.style.height = '100%'; + domNode.appendChild(div); + + renderTarget.current = div; + firstRender.current = true; + }, [domNode]); + + useDebounce(() => handlers.current.resize({ height, width }), 150, [height, width]); + + useEffect( + () => () => { + handlers.current.destroy(); + }, + [] + ); + + const render = useCallback(() => { + renderFn(renderTarget.current!, config, handlers.current); + }, [renderTarget, config, renderFn]); + + useEffect(() => { + if (!domNode) { + return; + } + + if (!reuseNode || !renderTarget.current) { + resetRenderTarget(); + } + + try { + render(); + firstRender.current = false; + } catch (err) { + onError(err, { title: strings.getRenderErrorMessage(functionName) }); + } + }, [domNode, functionName, onError, render, resetRenderTarget, reuseNode]); + + return ( +
+ { + setDomNode(node); + }} + /> +
+ ); +}; diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts new file mode 100644 index 0000000000000..4e0c7b217d5b7 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual } from 'lodash'; +// @ts-ignore untyped local +import { setFilter } from '../state/actions/elements'; +import { updateEmbeddableExpression, fetchEmbeddableRenderable } from '../state/actions/embeddable'; +import { RendererHandlers, CanvasElement } from '../../types'; + +// This class creates stub handlers to ensure every element and renderer fulfills the contract. +// TODO: consider warning if these methods are invoked but not implemented by the renderer...? + +export const createHandlers = (): RendererHandlers => ({ + destroy() {}, + done() {}, + event() {}, + getElementId() { + return ''; + }, + getFilter() { + return ''; + }, + onComplete(fn: () => void) { + this.done = fn; + }, + onDestroy(fn: () => void) { + this.destroy = fn; + }, + // TODO: these functions do not match the `onXYZ` and `xyz` pattern elsewhere. + onEmbeddableDestroyed() {}, + onEmbeddableInputChange() {}, + onResize(fn: (size: { height: number; width: number }) => void) { + this.resize = fn; + }, + reload() {}, + resize(_size: { height: number; width: number }) {}, + setFilter() {}, + update() {}, +}); + +export const assignHandlers = (handlers: Partial = {}): RendererHandlers => + Object.assign(createHandlers(), handlers); + +// TODO: this is a legacy approach we should unravel in the near future. +export const createDispatchedHandlerFactory = ( + dispatch: (action: any) => void +): ((element: CanvasElement) => RendererHandlers) => { + let isComplete = false; + let oldElement: CanvasElement | undefined; + let completeFn = () => {}; + + return (element: CanvasElement) => { + // reset isComplete when element changes + if (!isEqual(oldElement, element)) { + isComplete = false; + oldElement = element; + } + + return assignHandlers({ + setFilter(text: string) { + dispatch(setFilter(text, element.id, true)); + }, + + getFilter() { + return element.filter; + }, + + onComplete(fn: () => void) { + completeFn = fn; + }, + + getElementId: () => element.id, + + onEmbeddableInputChange(embeddableExpression: string) { + dispatch(updateEmbeddableExpression({ elementId: element.id, embeddableExpression })); + }, + + onEmbeddableDestroyed() { + dispatch(fetchEmbeddableRenderable(element.id)); + }, + + done() { + // don't emit if the element is already done + if (isComplete) { + return; + } + + isComplete = true; + completeFn(); + }, + }); + }; +}; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx b/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx index 5741f5f2d698c..6bcc0db92f1cc 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx @@ -7,13 +7,13 @@ import React, { FC, PureComponent } from 'react'; // @ts-expect-error untyped library import Style from 'style-it'; -// @ts-expect-error untyped local import { Positionable } from '../../public/components/positionable/positionable'; // @ts-expect-error untyped local import { elementToShape } from '../../public/components/workpad_page/utils'; import { CanvasRenderedElement } from '../types'; import { CanvasShareableContext, useCanvasShareableState } from '../context'; import { RendererSpec } from '../../types'; +import { createHandlers } from '../../public/lib/create_handlers'; import css from './rendered_element.module.scss'; @@ -62,17 +62,7 @@ export class RenderedElementComponent extends PureComponent { } try { - // TODO: These are stubbed, but may need implementation. - fn.render(this.ref.current, value.value, { - done: () => {}, - onDestroy: () => {}, - onResize: () => {}, - getElementId: () => '', - setFilter: () => {}, - getFilter: () => '', - onEmbeddableInputChange: () => {}, - onEmbeddableDestroyed: () => {}, - }); + fn.render(this.ref.current, value.value, createHandlers()); } catch (e) { // eslint-disable-next-line no-console console.log(as, e.message); diff --git a/x-pack/plugins/canvas/types/renderers.ts b/x-pack/plugins/canvas/types/renderers.ts index 2564b045d1cf7..772a16aa94c60 100644 --- a/x-pack/plugins/canvas/types/renderers.ts +++ b/x-pack/plugins/canvas/types/renderers.ts @@ -4,25 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -type GenericCallback = (callback: () => void) => void; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; -export interface RendererHandlers { - /** Handler to invoke when an element has finished rendering */ - done: () => void; +type GenericRendererCallback = (callback: () => void) => void; + +export interface RendererHandlers extends IInterpreterRenderHandlers { + /** Handler to invoke when an element should be destroyed. */ + destroy: () => void; /** Get the id of the element being rendered. Can be used as a unique ID in a render function */ getElementId: () => string; - /** Handler to invoke when an element is deleted or changes to a different render type */ - onDestroy: GenericCallback; - /** Handler to invoke when an element's dimensions have changed*/ - onResize: GenericCallback; /** Retrieves the value of the filter property on the element object persisted on the workpad */ getFilter: () => string; - /** Sets the value of the filter property on the element object persisted on the workpad */ - setFilter: (filter: string) => void; - /** Handler to invoke when the input to a function has changed internally */ - onEmbeddableInputChange: (expression: string) => void; + /** Handler to invoke when a renderer is considered complete */ + onComplete: (fn: () => void) => void; /** Handler to invoke when a rendered embeddable is destroyed */ onEmbeddableDestroyed: () => void; + /** Handler to invoke when the input to a function has changed internally */ + onEmbeddableInputChange: (expression: string) => void; + /** Handler to invoke when an element's dimensions have changed*/ + onResize: GenericRendererCallback; + /** Handler to invoke when an element should be resized. */ + resize: (size: { height: number; width: number }) => void; + /** Sets the value of the filter property on the element object persisted on the workpad */ + setFilter: (filter: string) => void; } export interface RendererSpec { From 312db71c41a7bfb9144a2bba1650ce917180c72b Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Wed, 24 Jun 2020 19:21:38 -0400 Subject: [PATCH 15/82] Revert "[7.x] Convert Positionable, RenderToDom and RenderWithFn to functional/hooks/no recompose. (#68202)" (#69865) This reverts commit 000c14ed19c244fcbcf1a67e3007d82fd4e14d5b. --- .../element_content/element_content.js | 12 +- .../element_wrapper/element_wrapper.js | 8 +- .../components/element_wrapper/index.js | 4 +- .../element_wrapper/lib/handlers.js | 60 +++++++++ .../positionable/{index.ts => index.js} | 5 +- .../components/positionable/positionable.js | 42 +++++++ .../components/positionable/positionable.tsx | 48 ------- .../public/components/render_to_dom/index.js | 12 ++ .../public/components/render_to_dom/index.ts | 7 -- .../components/render_to_dom/render_to_dom.js | 40 ++++++ .../render_to_dom/render_to_dom.tsx | 27 ---- .../public/components/render_with_fn/index.js | 30 +++++ .../{index.ts => lib/handlers.js} | 14 ++- .../render_with_fn/render_with_fn.tsx | 117 ------------------ .../canvas/public/lib/create_handlers.ts | 96 -------------- .../components/rendered_element.tsx | 14 ++- x-pack/plugins/canvas/types/renderers.ts | 28 ++--- 17 files changed, 233 insertions(+), 331 deletions(-) create mode 100644 x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js rename x-pack/plugins/canvas/public/components/positionable/{index.ts => index.js} (63%) create mode 100644 x-pack/plugins/canvas/public/components/positionable/positionable.js delete mode 100644 x-pack/plugins/canvas/public/components/positionable/positionable.tsx create mode 100644 x-pack/plugins/canvas/public/components/render_to_dom/index.js delete mode 100644 x-pack/plugins/canvas/public/components/render_to_dom/index.ts create mode 100644 x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js delete mode 100644 x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx create mode 100644 x-pack/plugins/canvas/public/components/render_with_fn/index.js rename x-pack/plugins/canvas/public/components/render_with_fn/{index.ts => lib/handlers.js} (61%) delete mode 100644 x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx delete mode 100644 x-pack/plugins/canvas/public/lib/create_handlers.ts diff --git a/x-pack/plugins/canvas/public/components/element_content/element_content.js b/x-pack/plugins/canvas/public/components/element_content/element_content.js index e2c1a61c348d1..114a457d167e7 100644 --- a/x-pack/plugins/canvas/public/components/element_content/element_content.js +++ b/x-pack/plugins/canvas/public/components/element_content/element_content.js @@ -12,7 +12,6 @@ import { getType } from '@kbn/interpreter/common'; import { Loading } from '../loading'; import { RenderWithFn } from '../render_with_fn'; import { ElementShareContainer } from '../element_share_container'; -import { assignHandlers } from '../../lib/create_handlers'; import { InvalidExpression } from './invalid_expression'; import { InvalidElementType } from './invalid_element_type'; @@ -47,7 +46,7 @@ const branches = [ export const ElementContent = compose( pure, ...branches -)(({ renderable, renderFunction, width, height, handlers }) => { +)(({ renderable, renderFunction, size, handlers }) => { const { getFilter, setFilter, @@ -63,7 +62,7 @@ export const ElementContent = compose(
diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js b/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js index de7748413b718..845fc5927d839 100644 --- a/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js +++ b/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js @@ -14,13 +14,7 @@ export const ElementWrapper = (props) => { return ( - + ); }; diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/index.js b/x-pack/plugins/canvas/public/components/element_wrapper/index.js index 6fc582bfee444..390c349ab2ee6 100644 --- a/x-pack/plugins/canvas/public/components/element_wrapper/index.js +++ b/x-pack/plugins/canvas/public/components/element_wrapper/index.js @@ -10,12 +10,12 @@ import { compose, withPropsOnChange, mapProps } from 'recompose'; import isEqual from 'react-fast-compare'; import { getResolvedArgs, getSelectedPage } from '../../state/selectors/workpad'; import { getState, getValue } from '../../lib/resolved_arg'; -import { createDispatchedHandlerFactory } from '../../lib/create_handlers'; import { ElementWrapper as Component } from './element_wrapper'; +import { createHandlers as createHandlersWithDispatch } from './lib/handlers'; function selectorFactory(dispatch) { let result = {}; - const createHandlers = createDispatchedHandlerFactory(dispatch); + const createHandlers = createHandlersWithDispatch(dispatch); return (nextState, nextOwnProps) => { const { element, ...restOwnProps } = nextOwnProps; diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js b/x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js new file mode 100644 index 0000000000000..33e8eacd902dd --- /dev/null +++ b/x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual } from 'lodash'; +import { setFilter } from '../../../state/actions/elements'; +import { + updateEmbeddableExpression, + fetchEmbeddableRenderable, +} from '../../../state/actions/embeddable'; + +export const createHandlers = (dispatch) => { + let isComplete = false; + let oldElement; + let completeFn = () => {}; + + return (element) => { + // reset isComplete when element changes + if (!isEqual(oldElement, element)) { + isComplete = false; + oldElement = element; + } + + return { + setFilter(text) { + dispatch(setFilter(text, element.id, true)); + }, + + getFilter() { + return element.filter; + }, + + onComplete(fn) { + completeFn = fn; + }, + + getElementId: () => element.id, + + onEmbeddableInputChange(embeddableExpression) { + dispatch(updateEmbeddableExpression({ elementId: element.id, embeddableExpression })); + }, + + onEmbeddableDestroyed() { + dispatch(fetchEmbeddableRenderable(element.id)); + }, + + done() { + // don't emit if the element is already done + if (isComplete) { + return; + } + + isComplete = true; + completeFn(); + }, + }; + }; +}; diff --git a/x-pack/plugins/canvas/public/components/positionable/index.ts b/x-pack/plugins/canvas/public/components/positionable/index.js similarity index 63% rename from x-pack/plugins/canvas/public/components/positionable/index.ts rename to x-pack/plugins/canvas/public/components/positionable/index.js index 964e2ee41df75..e5c3c32acb024 100644 --- a/x-pack/plugins/canvas/public/components/positionable/index.ts +++ b/x-pack/plugins/canvas/public/components/positionable/index.js @@ -4,4 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Positionable } from './positionable'; +import { pure } from 'recompose'; +import { Positionable as Component } from './positionable'; + +export const Positionable = pure(Component); diff --git a/x-pack/plugins/canvas/public/components/positionable/positionable.js b/x-pack/plugins/canvas/public/components/positionable/positionable.js new file mode 100644 index 0000000000000..9898f50cbb0f0 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/positionable/positionable.js @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { matrixToCSS } from '../../lib/dom'; + +export const Positionable = ({ children, transformMatrix, width, height }) => { + // Throw if there is more than one child + React.Children.only(children); + // This could probably be made nicer by having just one child + const wrappedChildren = React.Children.map(children, (child) => { + const newStyle = { + width, + height, + marginLeft: -width / 2, + marginTop: -height / 2, + position: 'absolute', + transform: matrixToCSS(transformMatrix.map((n, i) => (i < 12 ? n : Math.round(n)))), + }; + + const stepChild = React.cloneElement(child, { size: { width, height } }); + return ( +
+ {stepChild} +
+ ); + }); + + return wrappedChildren; +}; + +Positionable.propTypes = { + onChange: PropTypes.func, + children: PropTypes.element.isRequired, + transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/positionable/positionable.tsx b/x-pack/plugins/canvas/public/components/positionable/positionable.tsx deleted file mode 100644 index 3344398b00198..0000000000000 --- a/x-pack/plugins/canvas/public/components/positionable/positionable.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC, ReactElement, CSSProperties } from 'react'; -import PropTypes from 'prop-types'; -import { matrixToCSS } from '../../lib/dom'; -import { TransformMatrix3d } from '../../lib/aeroelastic'; - -interface Props { - children: ReactElement; - transformMatrix: TransformMatrix3d; - height: number; - width: number; -} - -export const Positionable: FC = ({ children, transformMatrix, width, height }) => { - // Throw if there is more than one child - const childNode = React.Children.only(children); - - const matrix = (transformMatrix.map((n, i) => - i < 12 ? n : Math.round(n) - ) as any) as TransformMatrix3d; - - const newStyle: CSSProperties = { - width, - height, - marginLeft: -width / 2, - marginTop: -height / 2, - position: 'absolute', - transform: matrixToCSS(matrix), - }; - - return ( -
- {childNode} -
- ); -}; - -Positionable.propTypes = { - children: PropTypes.element.isRequired, - transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, -}; diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/index.js b/x-pack/plugins/canvas/public/components/render_to_dom/index.js new file mode 100644 index 0000000000000..e8a3f8cd8c93b --- /dev/null +++ b/x-pack/plugins/canvas/public/components/render_to_dom/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { compose, withState } from 'recompose'; +import { RenderToDom as Component } from './render_to_dom'; + +export const RenderToDom = compose( + withState('domNode', 'setDomNode') // Still don't like this, seems to be the only way todo it. +)(Component); diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/index.ts b/x-pack/plugins/canvas/public/components/render_to_dom/index.ts deleted file mode 100644 index 43a5dad059c95..0000000000000 --- a/x-pack/plugins/canvas/public/components/render_to_dom/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { RenderToDom } from './render_to_dom'; diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js b/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js new file mode 100644 index 0000000000000..db393a8dde4f9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js @@ -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 from 'react'; +import PropTypes from 'prop-types'; + +export class RenderToDom extends React.Component { + static propTypes = { + domNode: PropTypes.object, + setDomNode: PropTypes.func.isRequired, + render: PropTypes.func.isRequired, + style: PropTypes.object, + }; + + shouldComponentUpdate(nextProps) { + return this.props.domNode !== nextProps.domNode; + } + + componentDidUpdate() { + // Calls render function once we have the reference to the DOM element to render into + if (this.props.domNode) { + this.props.render(this.props.domNode); + } + } + + render() { + const { domNode, setDomNode, style } = this.props; + const linkRef = (refNode) => { + if (!domNode && refNode) { + // Initialize the domNode property. This should only happen once, even if config changes. + setDomNode(refNode); + } + }; + + return
; + } +} diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx b/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx deleted file mode 100644 index a37c0fc096e57..0000000000000 --- a/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback, FC } from 'react'; -import CSS from 'csstype'; - -interface Props { - render: (element: HTMLElement) => void; - style?: CSS.Properties; -} - -export const RenderToDom: FC = ({ render, style }) => { - // https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node - const ref = useCallback( - (node: HTMLDivElement) => { - if (node !== null) { - render(node); - } - }, - [render] - ); - - return
; -}; diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/index.js b/x-pack/plugins/canvas/public/components/render_with_fn/index.js new file mode 100644 index 0000000000000..37c49624a3940 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/render_with_fn/index.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { compose, withProps, withPropsOnChange } from 'recompose'; +import PropTypes from 'prop-types'; +import isEqual from 'react-fast-compare'; +import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { RenderWithFn as Component } from './render_with_fn'; +import { ElementHandlers } from './lib/handlers'; + +export const RenderWithFn = compose( + withPropsOnChange( + // rebuild elementHandlers when handlers object changes + (props, nextProps) => !isEqual(props.handlers, nextProps.handlers), + ({ handlers }) => ({ + handlers: Object.assign(new ElementHandlers(), handlers), + }) + ), + withKibana, + withProps((props) => ({ + onError: props.kibana.services.canvas.notify.error, + })) +)(Component); + +RenderWithFn.propTypes = { + handlers: PropTypes.object, +}; diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/index.ts b/x-pack/plugins/canvas/public/components/render_with_fn/lib/handlers.js similarity index 61% rename from x-pack/plugins/canvas/public/components/render_with_fn/index.ts rename to x-pack/plugins/canvas/public/components/render_with_fn/lib/handlers.js index 4bfef734d34f4..9e5032efa97e2 100644 --- a/x-pack/plugins/canvas/public/components/render_with_fn/index.ts +++ b/x-pack/plugins/canvas/public/components/render_with_fn/lib/handlers.js @@ -4,4 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -export { RenderWithFn } from './render_with_fn'; +export class ElementHandlers { + resize() {} + + destroy() {} + + onResize(fn) { + this.resize = fn; + } + + onDestroy(fn) { + this.destroy = fn; + } +} diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx deleted file mode 100644 index bc51128cf0c87..0000000000000 --- a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState, useEffect, useRef, FC, useCallback } from 'react'; -import { useDebounce } from 'react-use'; - -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { RenderToDom } from '../render_to_dom'; -import { ErrorStrings } from '../../../i18n'; -import { RendererHandlers } from '../../../types'; - -const { RenderWithFn: strings } = ErrorStrings; - -interface Props { - name: string; - renderFn: ( - domNode: HTMLElement, - config: Record, - handlers: RendererHandlers - ) => void | Promise; - reuseNode: boolean; - handlers: RendererHandlers; - config: Record; - height: number; - width: number; -} - -const style = { height: '100%', width: '100%' }; - -export const RenderWithFn: FC = ({ - name: functionName, - renderFn, - reuseNode = false, - handlers: incomingHandlers, - config, - width, - height, -}) => { - const { services } = useKibana(); - const onError = services.canvas.notify.error; - - const [domNode, setDomNode] = useState(null); - - // Tells us if the component is attempting to re-render into a previously-populated render target. - const firstRender = useRef(true); - // A reference to the node appended to the provided DOM node which is created and optionally replaced. - const renderTarget = useRef(null); - // A reference to the handlers, as the renderFn may mutate them, (via onXYZ functions) - const handlers = useRef(incomingHandlers); - - // Reset the render target, the node appended to the DOM node provided by RenderToDOM. - const resetRenderTarget = useCallback(() => { - if (!domNode) { - return; - } - - if (!firstRender) { - handlers.current.destroy(); - } - - while (domNode.firstChild) { - domNode.removeChild(domNode.firstChild); - } - - const div = document.createElement('div'); - div.style.width = '100%'; - div.style.height = '100%'; - domNode.appendChild(div); - - renderTarget.current = div; - firstRender.current = true; - }, [domNode]); - - useDebounce(() => handlers.current.resize({ height, width }), 150, [height, width]); - - useEffect( - () => () => { - handlers.current.destroy(); - }, - [] - ); - - const render = useCallback(() => { - renderFn(renderTarget.current!, config, handlers.current); - }, [renderTarget, config, renderFn]); - - useEffect(() => { - if (!domNode) { - return; - } - - if (!reuseNode || !renderTarget.current) { - resetRenderTarget(); - } - - try { - render(); - firstRender.current = false; - } catch (err) { - onError(err, { title: strings.getRenderErrorMessage(functionName) }); - } - }, [domNode, functionName, onError, render, resetRenderTarget, reuseNode]); - - return ( -
- { - setDomNode(node); - }} - /> -
- ); -}; diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts deleted file mode 100644 index 4e0c7b217d5b7..0000000000000 --- a/x-pack/plugins/canvas/public/lib/create_handlers.ts +++ /dev/null @@ -1,96 +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 { isEqual } from 'lodash'; -// @ts-ignore untyped local -import { setFilter } from '../state/actions/elements'; -import { updateEmbeddableExpression, fetchEmbeddableRenderable } from '../state/actions/embeddable'; -import { RendererHandlers, CanvasElement } from '../../types'; - -// This class creates stub handlers to ensure every element and renderer fulfills the contract. -// TODO: consider warning if these methods are invoked but not implemented by the renderer...? - -export const createHandlers = (): RendererHandlers => ({ - destroy() {}, - done() {}, - event() {}, - getElementId() { - return ''; - }, - getFilter() { - return ''; - }, - onComplete(fn: () => void) { - this.done = fn; - }, - onDestroy(fn: () => void) { - this.destroy = fn; - }, - // TODO: these functions do not match the `onXYZ` and `xyz` pattern elsewhere. - onEmbeddableDestroyed() {}, - onEmbeddableInputChange() {}, - onResize(fn: (size: { height: number; width: number }) => void) { - this.resize = fn; - }, - reload() {}, - resize(_size: { height: number; width: number }) {}, - setFilter() {}, - update() {}, -}); - -export const assignHandlers = (handlers: Partial = {}): RendererHandlers => - Object.assign(createHandlers(), handlers); - -// TODO: this is a legacy approach we should unravel in the near future. -export const createDispatchedHandlerFactory = ( - dispatch: (action: any) => void -): ((element: CanvasElement) => RendererHandlers) => { - let isComplete = false; - let oldElement: CanvasElement | undefined; - let completeFn = () => {}; - - return (element: CanvasElement) => { - // reset isComplete when element changes - if (!isEqual(oldElement, element)) { - isComplete = false; - oldElement = element; - } - - return assignHandlers({ - setFilter(text: string) { - dispatch(setFilter(text, element.id, true)); - }, - - getFilter() { - return element.filter; - }, - - onComplete(fn: () => void) { - completeFn = fn; - }, - - getElementId: () => element.id, - - onEmbeddableInputChange(embeddableExpression: string) { - dispatch(updateEmbeddableExpression({ elementId: element.id, embeddableExpression })); - }, - - onEmbeddableDestroyed() { - dispatch(fetchEmbeddableRenderable(element.id)); - }, - - done() { - // don't emit if the element is already done - if (isComplete) { - return; - } - - isComplete = true; - completeFn(); - }, - }); - }; -}; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx b/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx index 6bcc0db92f1cc..5741f5f2d698c 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx @@ -7,13 +7,13 @@ import React, { FC, PureComponent } from 'react'; // @ts-expect-error untyped library import Style from 'style-it'; +// @ts-expect-error untyped local import { Positionable } from '../../public/components/positionable/positionable'; // @ts-expect-error untyped local import { elementToShape } from '../../public/components/workpad_page/utils'; import { CanvasRenderedElement } from '../types'; import { CanvasShareableContext, useCanvasShareableState } from '../context'; import { RendererSpec } from '../../types'; -import { createHandlers } from '../../public/lib/create_handlers'; import css from './rendered_element.module.scss'; @@ -62,7 +62,17 @@ export class RenderedElementComponent extends PureComponent { } try { - fn.render(this.ref.current, value.value, createHandlers()); + // TODO: These are stubbed, but may need implementation. + fn.render(this.ref.current, value.value, { + done: () => {}, + onDestroy: () => {}, + onResize: () => {}, + getElementId: () => '', + setFilter: () => {}, + getFilter: () => '', + onEmbeddableInputChange: () => {}, + onEmbeddableDestroyed: () => {}, + }); } catch (e) { // eslint-disable-next-line no-console console.log(as, e.message); diff --git a/x-pack/plugins/canvas/types/renderers.ts b/x-pack/plugins/canvas/types/renderers.ts index 772a16aa94c60..2564b045d1cf7 100644 --- a/x-pack/plugins/canvas/types/renderers.ts +++ b/x-pack/plugins/canvas/types/renderers.ts @@ -4,29 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +type GenericCallback = (callback: () => void) => void; -type GenericRendererCallback = (callback: () => void) => void; - -export interface RendererHandlers extends IInterpreterRenderHandlers { - /** Handler to invoke when an element should be destroyed. */ - destroy: () => void; +export interface RendererHandlers { + /** Handler to invoke when an element has finished rendering */ + done: () => void; /** Get the id of the element being rendered. Can be used as a unique ID in a render function */ getElementId: () => string; + /** Handler to invoke when an element is deleted or changes to a different render type */ + onDestroy: GenericCallback; + /** Handler to invoke when an element's dimensions have changed*/ + onResize: GenericCallback; /** Retrieves the value of the filter property on the element object persisted on the workpad */ getFilter: () => string; - /** Handler to invoke when a renderer is considered complete */ - onComplete: (fn: () => void) => void; - /** Handler to invoke when a rendered embeddable is destroyed */ - onEmbeddableDestroyed: () => void; - /** Handler to invoke when the input to a function has changed internally */ - onEmbeddableInputChange: (expression: string) => void; - /** Handler to invoke when an element's dimensions have changed*/ - onResize: GenericRendererCallback; - /** Handler to invoke when an element should be resized. */ - resize: (size: { height: number; width: number }) => void; /** Sets the value of the filter property on the element object persisted on the workpad */ setFilter: (filter: string) => void; + /** Handler to invoke when the input to a function has changed internally */ + onEmbeddableInputChange: (expression: string) => void; + /** Handler to invoke when a rendered embeddable is destroyed */ + onEmbeddableDestroyed: () => void; } export interface RendererSpec { From bdd66be0ec99fb5da2949a31c585cdb8bd820e32 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 24 Jun 2020 16:21:46 -0700 Subject: [PATCH 16/82] [SECURITY] Disables Cypress tests Signed-off-by: Tyler Smalley --- test/scripts/jenkins_security_solution_cypress.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/scripts/jenkins_security_solution_cypress.sh b/test/scripts/jenkins_security_solution_cypress.sh index 23b83cf946d49..8aa3425be0beb 100644 --- a/test/scripts/jenkins_security_solution_cypress.sh +++ b/test/scripts/jenkins_security_solution_cypress.sh @@ -11,11 +11,16 @@ export KIBANA_INSTALL_DIR="$destDir" echo " -> Running security solution cypress tests" cd "$XPACK_DIR" -checks-reporter-with-killswitch "Security solution Cypress Tests" \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$KIBANA_INSTALL_DIR" \ - --config test/security_solution_cypress/config.ts +# Failures across multiple suites, skipping all +# https://github.com/elastic/kibana/issues/69847 +# https://github.com/elastic/kibana/issues/69848 +# https://github.com/elastic/kibana/issues/69849 + +# checks-reporter-with-killswitch "Security solution Cypress Tests" \ +# node scripts/functional_tests \ +# --debug --bail \ +# --kibana-install-dir "$KIBANA_INSTALL_DIR" \ +# --config test/security_solution_cypress/config.ts echo "" echo "" From 7d47e96837f80a922f7514e663734c46dde0e6db Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Wed, 24 Jun 2020 19:23:20 -0400 Subject: [PATCH 17/82] [7.x] [pre-req] Convert Palettes and Components (#69065) (#69819) Co-authored-by: Elastic Machine --- x-pack/plugins/canvas/.storybook/config.js | 3 + .../canvas/.storybook/webpack.config.js | 1 + .../functions/common/palette.test.js | 10 +- .../functions/common/palette.ts | 5 +- .../__snapshots__/palette.stories.storyshot | 86 ++++++ .../__examples__/palette.stories.tsx | 32 +++ .../canvas_plugin_src/uis/arguments/index.ts | 1 - .../uis/arguments/{palette.js => palette.tsx} | 62 +++-- x-pack/plugins/canvas/common/lib/index.ts | 1 - x-pack/plugins/canvas/common/lib/palettes.js | 152 ---------- x-pack/plugins/canvas/common/lib/palettes.ts | 263 ++++++++++++++++++ x-pack/plugins/canvas/i18n/components.ts | 10 + x-pack/plugins/canvas/i18n/constants.ts | 1 + x-pack/plugins/canvas/i18n/index.ts | 1 + x-pack/plugins/canvas/i18n/lib.ts | 92 ++++++ x-pack/plugins/canvas/i18n/ui.ts | 6 +- .../palette_picker.stories.storyshot | 237 ++++++++++++++++ .../__examples__/palette_picker.stories.tsx | 25 ++ .../public/components/palette_picker/index.js | 11 - .../index.js => palette_picker/index.ts} | 6 +- .../palette_picker/palette_picker.js | 60 ---- .../palette_picker/palette_picker.scss | 42 --- .../palette_picker/palette_picker.tsx | 92 ++++++ .../palette_swatch/palette_swatch.js | 46 --- .../palette_swatch/palette_swatch.scss | 35 --- x-pack/plugins/canvas/public/style/index.scss | 2 - 26 files changed, 898 insertions(+), 384 deletions(-) create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/{palette.js => palette.tsx} (58%) delete mode 100644 x-pack/plugins/canvas/common/lib/palettes.js create mode 100644 x-pack/plugins/canvas/common/lib/palettes.ts create mode 100644 x-pack/plugins/canvas/i18n/lib.ts create mode 100644 x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot create mode 100644 x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.tsx delete mode 100644 x-pack/plugins/canvas/public/components/palette_picker/index.js rename x-pack/plugins/canvas/public/components/{palette_swatch/index.js => palette_picker/index.ts} (62%) delete mode 100644 x-pack/plugins/canvas/public/components/palette_picker/palette_picker.js delete mode 100644 x-pack/plugins/canvas/public/components/palette_picker/palette_picker.scss create mode 100644 x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx delete mode 100644 x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.js delete mode 100644 x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.scss diff --git a/x-pack/plugins/canvas/.storybook/config.js b/x-pack/plugins/canvas/.storybook/config.js index c808a672711ab..04b4e2a8e7b4b 100644 --- a/x-pack/plugins/canvas/.storybook/config.js +++ b/x-pack/plugins/canvas/.storybook/config.js @@ -59,6 +59,9 @@ function loadStories() { // Find all files ending in *.examples.ts const req = require.context('./..', true, /.(stories|examples).tsx$/); req.keys().forEach(filename => req(filename)); + + // Import Canvas CSS + require('../public/style/index.scss') } // Set up the Storybook environment with custom settings. diff --git a/x-pack/plugins/canvas/.storybook/webpack.config.js b/x-pack/plugins/canvas/.storybook/webpack.config.js index 4d83a3d4fa70f..45a5303d8b0db 100644 --- a/x-pack/plugins/canvas/.storybook/webpack.config.js +++ b/x-pack/plugins/canvas/.storybook/webpack.config.js @@ -199,6 +199,7 @@ module.exports = async ({ config }) => { config.resolve.alias['ui/url/absolute_to_parsed_url'] = path.resolve(__dirname, '../tasks/mocks/uiAbsoluteToParsedUrl'); config.resolve.alias['ui/chrome'] = path.resolve(__dirname, '../tasks/mocks/uiChrome'); config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'); + config.resolve.alias['src/legacy/ui/public/styles/styling_constants'] = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss'); config.resolve.alias.ng_mock$ = path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'); return config; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.test.js index af03297ad666b..01cabd171c2fe 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.test.js @@ -5,7 +5,7 @@ */ import { functionWrapper } from '../../../__tests__/helpers/function_wrapper'; -import { palettes } from '../../../common/lib/palettes'; +import { paulTor14 } from '../../../common/lib/palettes'; import { palette } from './palette'; describe('palette', () => { @@ -25,7 +25,7 @@ describe('palette', () => { it('defaults to pault_tor_14 colors', () => { const result = fn(null); - expect(result.colors).toEqual(palettes.paul_tor_14.colors); + expect(result.colors).toEqual(paulTor14.colors); }); }); @@ -47,17 +47,17 @@ describe('palette', () => { describe('reverse', () => { it('reverses order of the colors', () => { const result = fn(null, { reverse: true }); - expect(result.colors).toEqual(palettes.paul_tor_14.colors.reverse()); + expect(result.colors).toEqual(paulTor14.colors.reverse()); }); it('keeps the original order of the colors', () => { const result = fn(null, { reverse: false }); - expect(result.colors).toEqual(palettes.paul_tor_14.colors); + expect(result.colors).toEqual(paulTor14.colors); }); it(`defaults to 'false`, () => { const result = fn(null); - expect(result.colors).toEqual(palettes.paul_tor_14.colors); + expect(result.colors).toEqual(paulTor14.colors); }); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.ts index f27abe261e2e2..50d62a19b2361 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/palette.ts @@ -5,8 +5,7 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -// @ts-expect-error untyped local -import { palettes } from '../../../common/lib/palettes'; +import { paulTor14 } from '../../../common/lib/palettes'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { @@ -52,7 +51,7 @@ export function palette(): ExpressionFunctionDefinition<'palette', null, Argumen }, fn: (input, args) => { const { color, reverse, gradient } = args; - const colors = ([] as string[]).concat(color || palettes.paul_tor_14.colors); + const colors = ([] as string[]).concat(color || paulTor14.colors); return { type: 'palette', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot new file mode 100644 index 0000000000000..385b16d3d8e8e --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/__snapshots__/palette.stories.storyshot @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots arguments/Palette default 1`] = ` +
+
+
+ +
+
+ + Select an option: +
+ , is selected + + +
+ + +
+
+
+
+
+`; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx new file mode 100644 index 0000000000000..6bc285a3d66d2 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/__examples__/palette.stories.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { PaletteArgInput } from '../palette'; +import { paulTor14 } from '../../../../common/lib/palettes'; + +storiesOf('arguments/Palette', module).add('default', () => ( +
+ +
+)); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts index 94a9cf28aef69..ddf428d884917 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts @@ -15,7 +15,6 @@ import { imageUpload } from './image_upload'; // @ts-expect-error untyped local import { number } from './number'; import { numberFormatInitializer } from './number_format'; -// @ts-expect-error untyped local import { palette } from './palette'; // @ts-expect-error untyped local import { percentage } from './percentage'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx similarity index 58% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx index eddaa20a4800e..a33d000a1f656 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/palette.tsx @@ -4,45 +4,63 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FC } from 'react'; import PropTypes from 'prop-types'; import { get } from 'lodash'; import { getType } from '@kbn/interpreter/common'; +import { ExpressionAstFunction, ExpressionAstExpression } from 'src/plugins/expressions'; import { PalettePicker } from '../../../public/components/palette_picker'; import { templateFromReactComponent } from '../../../public/lib/template_from_react_component'; import { ArgumentStrings } from '../../../i18n'; +import { identifyPalette, ColorPalette } from '../../../common/lib'; const { Palette: strings } = ArgumentStrings; -const PaletteArgInput = ({ onValueChange, argValue, renderError }) => { - // Why is this neccesary? Does the dialog really need to know what parameter it is setting? - - const throwNotParsed = () => renderError(); +interface Props { + onValueChange: (value: ExpressionAstExpression) => void; + argValue: ExpressionAstExpression; + renderError: () => void; + argId?: string; +} +export const PaletteArgInput: FC = ({ onValueChange, argId, argValue, renderError }) => { // TODO: This is weird, its basically a reimplementation of what the interpretter would return. - // Probably a better way todo this, and maybe a better way to handle template stype objects in general? - function astToPalette({ chain }) { + // Probably a better way todo this, and maybe a better way to handle template type objects in general? + const astToPalette = ({ chain }: { chain: ExpressionAstFunction[] }): ColorPalette | null => { if (chain.length !== 1 || chain[0].function !== 'palette') { - throwNotParsed(); + renderError(); + return null; } + try { const colors = chain[0].arguments._.map((astObj) => { if (getType(astObj) !== 'string') { - throwNotParsed(); + renderError(); } return astObj; - }); + }) as string[]; - const gradient = get(chain[0].arguments.gradient, '[0]'); + const gradient = get(chain[0].arguments.gradient, '[0]'); + const palette = identifyPalette({ colors, gradient }); - return { colors, gradient }; + if (palette) { + return palette; + } + + return ({ + id: 'custom', + label: strings.getCustomPaletteLabel(), + colors, + gradient, + } as any) as ColorPalette; } catch (e) { - throwNotParsed(); + renderError(); } - } + return null; + }; - function handleChange(palette) { - const astObj = { + const handleChange = (palette: ColorPalette): void => { + const astObj: ExpressionAstExpression = { type: 'expression', chain: [ { @@ -57,16 +75,20 @@ const PaletteArgInput = ({ onValueChange, argValue, renderError }) => { }; onValueChange(astObj); - } + }; const palette = astToPalette(argValue); - return ( - - ); + if (!palette) { + renderError(); + return null; + } + + return ; }; PaletteArgInput.propTypes = { + argId: PropTypes.string, onValueChange: PropTypes.func.isRequired, argValue: PropTypes.any.isRequired, renderError: PropTypes.func, diff --git a/x-pack/plugins/canvas/common/lib/index.ts b/x-pack/plugins/canvas/common/lib/index.ts index 4cb3cbbb9b4e6..6bd7e0bc9948f 100644 --- a/x-pack/plugins/canvas/common/lib/index.ts +++ b/x-pack/plugins/canvas/common/lib/index.ts @@ -26,7 +26,6 @@ export * from './hex_to_rgb'; export * from './httpurl'; // @ts-expect-error missing local definition export * from './missing_asset'; -// @ts-expect-error missing local definition export * from './palettes'; export * from './pivot_object_array'; // @ts-expect-error missing local definition diff --git a/x-pack/plugins/canvas/common/lib/palettes.js b/x-pack/plugins/canvas/common/lib/palettes.js deleted file mode 100644 index 3fe977ec3862c..0000000000000 --- a/x-pack/plugins/canvas/common/lib/palettes.js +++ /dev/null @@ -1,152 +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. - */ - -/* - This should be pluggable -*/ - -export const palettes = { - paul_tor_14: { - colors: [ - '#882E72', - '#B178A6', - '#D6C1DE', - '#1965B0', - '#5289C7', - '#7BAFDE', - '#4EB265', - '#90C987', - '#CAE0AB', - '#F7EE55', - '#F6C141', - '#F1932D', - '#E8601C', - '#DC050C', - ], - gradient: false, - }, - paul_tor_21: { - colors: [ - '#771155', - '#AA4488', - '#CC99BB', - '#114477', - '#4477AA', - '#77AADD', - '#117777', - '#44AAAA', - '#77CCCC', - '#117744', - '#44AA77', - '#88CCAA', - '#777711', - '#AAAA44', - '#DDDD77', - '#774411', - '#AA7744', - '#DDAA77', - '#771122', - '#AA4455', - '#DD7788', - ], - gradient: false, - }, - earth_tones: { - colors: [ - '#842113', - '#984d23', - '#32221c', - '#739379', - '#dab150', - '#4d2521', - '#716c49', - '#bb3918', - '#7e5436', - '#c27c34', - '#72392e', - '#8f8b7e', - ], - gradient: false, - }, - canvas: { - colors: [ - '#01A4A4', - '#CC6666', - '#D0D102', - '#616161', - '#00A1CB', - '#32742C', - '#F18D05', - '#113F8C', - '#61AE24', - '#D70060', - ], - gradient: false, - }, - color_blind: { - colors: [ - '#1ea593', - '#2b70f7', - '#ce0060', - '#38007e', - '#fca5d3', - '#f37020', - '#e49e29', - '#b0916f', - '#7b000b', - '#34130c', - ], - gradient: false, - }, - elastic_teal: { - colors: ['#C5FAF4', '#0F6259'], - gradient: true, - }, - elastic_blue: { - colors: ['#7ECAE3', '#003A4D'], - gradient: true, - }, - elastic_yellow: { - colors: ['#FFE674', '#4D3F00'], - gradient: true, - }, - elastic_pink: { - colors: ['#FEA8D5', '#531E3A'], - gradient: true, - }, - elastic_green: { - colors: ['#D3FB71', '#131A00'], - gradient: true, - }, - elastic_orange: { - colors: ['#FFC68A', '#7B3F00'], - gradient: true, - }, - elastic_purple: { - colors: ['#CCC7DF', '#130351'], - gradient: true, - }, - green_blue_red: { - colors: ['#D3FB71', '#7ECAE3', '#f03b20'], - gradient: true, - }, - yellow_green: { - colors: ['#f7fcb9', '#addd8e', '#31a354'], - gradient: true, - }, - yellow_blue: { - colors: ['#edf8b1', '#7fcdbb', '#2c7fb8'], - gradient: true, - }, - yellow_red: { - colors: ['#ffeda0', '#feb24c', '#f03b20'], - gradient: true, - }, - instagram: { - colors: ['#833ab4', '#fd1d1d', '#fcb045'], - gradient: true, - }, -}; diff --git a/x-pack/plugins/canvas/common/lib/palettes.ts b/x-pack/plugins/canvas/common/lib/palettes.ts new file mode 100644 index 0000000000000..1469ba63967c0 --- /dev/null +++ b/x-pack/plugins/canvas/common/lib/palettes.ts @@ -0,0 +1,263 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual } from 'lodash'; +import { LibStrings } from '../../i18n'; + +const { Palettes: strings } = LibStrings; + +/** + * This type contains a unions of all supported palette ids. + */ +export type PaletteID = typeof palettes[number]['id']; + +/** + * An interface representing a color palette in Canvas, with a textual label and a set of + * hex values. + */ +export interface ColorPalette { + id: PaletteID; + label: string; + colors: string[]; + gradient: boolean; +} + +// This function allows one to create a strongly-typed palette for inclusion in +// the palette collection. As a result, the values and labels are known to the +// type system, preventing one from specifying a non-existent palette at build +// time. +function createPalette< + RawPalette extends { + id: RawPaletteID; + }, + RawPaletteID extends string +>(palette: RawPalette) { + return palette; +} + +/** + * Return a palette given a set of colors and gradient. Returns undefined if the + * palette doesn't match. + */ +export const identifyPalette = ( + input: Pick +): ColorPalette | undefined => { + return palettes.find((palette) => { + const { colors, gradient } = palette; + return gradient === input.gradient && isEqual(colors, input.colors); + }); +}; + +export const paulTor14 = createPalette({ + id: 'paul_tor_14', + label: 'Paul Tor 14', + colors: [ + '#882E72', + '#B178A6', + '#D6C1DE', + '#1965B0', + '#5289C7', + '#7BAFDE', + '#4EB265', + '#90C987', + '#CAE0AB', + '#F7EE55', + '#F6C141', + '#F1932D', + '#E8601C', + '#DC050C', + ], + gradient: false, +}); + +export const paulTor21 = createPalette({ + id: 'paul_tor_21', + label: 'Paul Tor 21', + colors: [ + '#771155', + '#AA4488', + '#CC99BB', + '#114477', + '#4477AA', + '#77AADD', + '#117777', + '#44AAAA', + '#77CCCC', + '#117744', + '#44AA77', + '#88CCAA', + '#777711', + '#AAAA44', + '#DDDD77', + '#774411', + '#AA7744', + '#DDAA77', + '#771122', + '#AA4455', + '#DD7788', + ], + gradient: false, +}); + +export const earthTones = createPalette({ + id: 'earth_tones', + label: strings.getEarthTones(), + colors: [ + '#842113', + '#984d23', + '#32221c', + '#739379', + '#dab150', + '#4d2521', + '#716c49', + '#bb3918', + '#7e5436', + '#c27c34', + '#72392e', + '#8f8b7e', + ], + gradient: false, +}); + +export const canvas = createPalette({ + id: 'canvas', + label: strings.getCanvas(), + colors: [ + '#01A4A4', + '#CC6666', + '#D0D102', + '#616161', + '#00A1CB', + '#32742C', + '#F18D05', + '#113F8C', + '#61AE24', + '#D70060', + ], + gradient: false, +}); + +export const colorBlind = createPalette({ + id: 'color_blind', + label: strings.getColorBlind(), + colors: [ + '#1ea593', + '#2b70f7', + '#ce0060', + '#38007e', + '#fca5d3', + '#f37020', + '#e49e29', + '#b0916f', + '#7b000b', + '#34130c', + ], + gradient: false, +}); + +export const elasticTeal = createPalette({ + id: 'elastic_teal', + label: strings.getElasticTeal(), + colors: ['#7ECAE3', '#003A4D'], + gradient: true, +}); + +export const elasticBlue = createPalette({ + id: 'elastic_blue', + label: strings.getElasticBlue(), + colors: ['#C5FAF4', '#0F6259'], + gradient: true, +}); + +export const elasticYellow = createPalette({ + id: 'elastic_yellow', + label: strings.getElasticYellow(), + colors: ['#FFE674', '#4D3F00'], + gradient: true, +}); + +export const elasticPink = createPalette({ + id: 'elastic_pink', + label: strings.getElasticPink(), + colors: ['#FEA8D5', '#531E3A'], + gradient: true, +}); + +export const elasticGreen = createPalette({ + id: 'elastic_green', + label: strings.getElasticGreen(), + colors: ['#D3FB71', '#131A00'], + gradient: true, +}); + +export const elasticOrange = createPalette({ + id: 'elastic_orange', + label: strings.getElasticOrange(), + colors: ['#FFC68A', '#7B3F00'], + gradient: true, +}); + +export const elasticPurple = createPalette({ + id: 'elastic_purple', + label: strings.getElasticPurple(), + colors: ['#CCC7DF', '#130351'], + gradient: true, +}); + +export const greenBlueRed = createPalette({ + id: 'green_blue_red', + label: strings.getGreenBlueRed(), + colors: ['#D3FB71', '#7ECAE3', '#f03b20'], + gradient: true, +}); + +export const yellowGreen = createPalette({ + id: 'yellow_green', + label: strings.getYellowGreen(), + colors: ['#f7fcb9', '#addd8e', '#31a354'], + gradient: true, +}); + +export const yellowBlue = createPalette({ + id: 'yellow_blue', + label: strings.getYellowBlue(), + colors: ['#edf8b1', '#7fcdbb', '#2c7fb8'], + gradient: true, +}); + +export const yellowRed = createPalette({ + id: 'yellow_red', + label: strings.getYellowRed(), + colors: ['#ffeda0', '#feb24c', '#f03b20'], + gradient: true, +}); + +export const instagram = createPalette({ + id: 'instagram', + label: strings.getInstagram(), + colors: ['#833ab4', '#fd1d1d', '#fcb045'], + gradient: true, +}); + +export const palettes = [ + paulTor14, + paulTor21, + earthTones, + canvas, + colorBlind, + elasticTeal, + elasticBlue, + elasticYellow, + elasticPink, + elasticGreen, + elasticOrange, + elasticPurple, + greenBlueRed, + yellowGreen, + yellowBlue, + yellowRed, + instagram, +]; diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index de16bc2101e8c..0b512c80b209b 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -586,6 +586,16 @@ export const ComponentStrings = { defaultMessage: 'Delete', }), }, + PalettePicker: { + getEmptyPaletteLabel: () => + i18n.translate('xpack.canvas.palettePicker.emptyPaletteLabel', { + defaultMessage: 'None', + }), + getNoPaletteFoundErrorTitle: () => + i18n.translate('xpack.canvas.palettePicker.noPaletteFoundErrorTitle', { + defaultMessage: 'Color palette not found', + }), + }, SavedElementsModal: { getAddNewElementDescription: () => i18n.translate('xpack.canvas.savedElementsModal.addNewElementDescription', { diff --git a/x-pack/plugins/canvas/i18n/constants.ts b/x-pack/plugins/canvas/i18n/constants.ts index 099effc697fc5..af82d0afc7e9f 100644 --- a/x-pack/plugins/canvas/i18n/constants.ts +++ b/x-pack/plugins/canvas/i18n/constants.ts @@ -20,6 +20,7 @@ export const FONT_FAMILY = '`font-family`'; export const FONT_WEIGHT = '`font-weight`'; export const HEX = 'HEX'; export const HTML = 'HTML'; +export const INSTAGRAM = 'Instagram'; export const ISO8601 = 'ISO8601'; export const JS = 'JavaScript'; export const JSON = 'JSON'; diff --git a/x-pack/plugins/canvas/i18n/index.ts b/x-pack/plugins/canvas/i18n/index.ts index 864311d34aca0..3bf1fa077130c 100644 --- a/x-pack/plugins/canvas/i18n/index.ts +++ b/x-pack/plugins/canvas/i18n/index.ts @@ -11,6 +11,7 @@ export * from './errors'; export * from './expression_types'; export * from './elements'; export * from './functions'; +export * from './lib'; export * from './renderers'; export * from './shortcuts'; export * from './tags'; diff --git a/x-pack/plugins/canvas/i18n/lib.ts b/x-pack/plugins/canvas/i18n/lib.ts new file mode 100644 index 0000000000000..eca6dc44354a2 --- /dev/null +++ b/x-pack/plugins/canvas/i18n/lib.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { CANVAS, INSTAGRAM } from './constants'; + +export const LibStrings = { + Palettes: { + getEarthTones: () => + i18n.translate('xpack.canvas.lib.palettes.earthTonesLabel', { + defaultMessage: 'Earth Tones', + }), + getCanvas: () => + i18n.translate('xpack.canvas.lib.palettes.canvasLabel', { + defaultMessage: '{CANVAS}', + values: { + CANVAS, + }, + }), + + getColorBlind: () => + i18n.translate('xpack.canvas.lib.palettes.colorBlindLabel', { + defaultMessage: 'Color Blind', + }), + + getElasticTeal: () => + i18n.translate('xpack.canvas.lib.palettes.elasticTealLabel', { + defaultMessage: 'Elastic Teal', + }), + + getElasticBlue: () => + i18n.translate('xpack.canvas.lib.palettes.elasticBlueLabel', { + defaultMessage: 'Elastic Blue', + }), + + getElasticYellow: () => + i18n.translate('xpack.canvas.lib.palettes.elasticYellowLabel', { + defaultMessage: 'Elastic Yellow', + }), + + getElasticPink: () => + i18n.translate('xpack.canvas.lib.palettes.elasticPinkLabel', { + defaultMessage: 'Elastic Pink', + }), + + getElasticGreen: () => + i18n.translate('xpack.canvas.lib.palettes.elasticGreenLabel', { + defaultMessage: 'Elastic Green', + }), + + getElasticOrange: () => + i18n.translate('xpack.canvas.lib.palettes.elasticOrangeLabel', { + defaultMessage: 'Elastic Orange', + }), + + getElasticPurple: () => + i18n.translate('xpack.canvas.lib.palettes.elasticPurpleLabel', { + defaultMessage: 'Elastic Purple', + }), + + getGreenBlueRed: () => + i18n.translate('xpack.canvas.lib.palettes.greenBlueRedLabel', { + defaultMessage: 'Green, Blue, Red', + }), + + getYellowGreen: () => + i18n.translate('xpack.canvas.lib.palettes.yellowGreenLabel', { + defaultMessage: 'Yellow, Green', + }), + + getYellowBlue: () => + i18n.translate('xpack.canvas.lib.palettes.yellowBlueLabel', { + defaultMessage: 'Yellow, Blue', + }), + + getYellowRed: () => + i18n.translate('xpack.canvas.lib.palettes.yellowRedLabel', { + defaultMessage: 'Yellow, Red', + }), + + getInstagram: () => + i18n.translate('xpack.canvas.lib.palettes.instagramLabel', { + defaultMessage: '{INSTAGRAM}', + values: { + INSTAGRAM, + }, + }), + }, +}; diff --git a/x-pack/plugins/canvas/i18n/ui.ts b/x-pack/plugins/canvas/i18n/ui.ts index f69f9e747ab90..bc282db203be2 100644 --- a/x-pack/plugins/canvas/i18n/ui.ts +++ b/x-pack/plugins/canvas/i18n/ui.ts @@ -232,7 +232,11 @@ export const ArgumentStrings = { }), getHelp: () => i18n.translate('xpack.canvas.uis.arguments.paletteLabel', { - defaultMessage: 'Choose a color palette', + defaultMessage: 'The collection of colors used to render the element', + }), + getCustomPaletteLabel: () => + i18n.translate('xpack.canvas.uis.arguments.customPaletteLabel', { + defaultMessage: 'Custom', }), }, Percentage: { diff --git a/x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot b/x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot new file mode 100644 index 0000000000000..d3809b4c3979f --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/__examples__/__snapshots__/palette_picker.stories.storyshot @@ -0,0 +1,237 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/Color/PalettePicker clearable 1`] = ` +
+
+
+ +
+
+ + Select an option: None, is selected + + +
+ + +
+
+
+
+
+`; + +exports[`Storyshots components/Color/PalettePicker default 1`] = ` +
+
+
+ +
+
+ + Select an option: +
+ , is selected + + +
+ + +
+
+
+
+
+`; + +exports[`Storyshots components/Color/PalettePicker interactive 1`] = ` +
+
+
+ +
+
+ + Select an option: +
+ , is selected + + +
+ + +
+
+
+
+
+`; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.tsx b/x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.tsx new file mode 100644 index 0000000000000..b1ae860e80efb --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/__examples__/palette_picker.stories.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 React, { FC, useState } from 'react'; +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import { PalettePicker } from '../palette_picker'; + +import { paulTor14, ColorPalette } from '../../../../common/lib/palettes'; + +const Interactive: FC = () => { + const [palette, setPalette] = useState(paulTor14); + return ; +}; + +storiesOf('components/Color/PalettePicker', module) + .addDecorator((fn) =>
{fn()}
) + .add('default', () => ) + .add('clearable', () => ( + + )) + .add('interactive', () => ); diff --git a/x-pack/plugins/canvas/public/components/palette_picker/index.js b/x-pack/plugins/canvas/public/components/palette_picker/index.js deleted file mode 100644 index 33d1d22777183..0000000000000 --- a/x-pack/plugins/canvas/public/components/palette_picker/index.js +++ /dev/null @@ -1,11 +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 { pure } from 'recompose'; - -import { PalettePicker as Component } from './palette_picker'; - -export const PalettePicker = pure(Component); diff --git a/x-pack/plugins/canvas/public/components/palette_swatch/index.js b/x-pack/plugins/canvas/public/components/palette_picker/index.ts similarity index 62% rename from x-pack/plugins/canvas/public/components/palette_swatch/index.js rename to x-pack/plugins/canvas/public/components/palette_picker/index.ts index 2be37a8338b2b..840600698c5a4 100644 --- a/x-pack/plugins/canvas/public/components/palette_swatch/index.js +++ b/x-pack/plugins/canvas/public/components/palette_picker/index.ts @@ -4,8 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pure } from 'recompose'; - -import { PaletteSwatch as Component } from './palette_swatch'; - -export const PaletteSwatch = pure(Component); +export { PalettePicker } from './palette_picker'; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.js b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.js deleted file mode 100644 index ca2a499feb84c..0000000000000 --- a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { map } from 'lodash'; -import { Popover } from '../popover'; -import { PaletteSwatch } from '../palette_swatch'; -import { palettes } from '../../../common/lib/palettes'; - -export const PalettePicker = ({ onChange, value, anchorPosition, ariaLabel }) => { - const button = (handleClick) => ( - - ); - - return ( - - {() => ( -
- {map(palettes, (palette, name) => ( - - ))} -
- )} -
- ); -}; - -PalettePicker.propTypes = { - value: PropTypes.object, - onChange: PropTypes.func, - anchorPosition: PropTypes.string, -}; diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.scss b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.scss deleted file mode 100644 index f837d47682f61..0000000000000 --- a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.scss +++ /dev/null @@ -1,42 +0,0 @@ -.canvasPalettePicker { - display: inline-block; - width: 100%; -} - -.canvasPalettePicker__swatches { - @include euiScrollBar; - - width: 280px; - height: 250px; - overflow-y: scroll; -} - -.canvasPalettePicker__swatchesPanel { - padding: $euiSizeS 0 !important; // sass-lint:disable-line no-important -} - -.canvasPalettePicker__swatch { - padding: $euiSizeS $euiSize; - - &:hover, - &:focus { - text-decoration: underline; - background-color: $euiColorLightestShade; - - .canvasPaletteSwatch, - .canvasPaletteSwatch__background { - transform: scaleY(2); - } - - .canvasPalettePicker__label { - color: $euiTextColor; - } - } -} - -.canvasPalettePicker__label { - font-size: $euiFontSizeXS; - text-transform: capitalize; - text-align: left; - color: $euiColorDarkShade; -} diff --git a/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx new file mode 100644 index 0000000000000..dec09a5335d95 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/palette_picker/palette_picker.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import PropTypes from 'prop-types'; +import { EuiColorPalettePicker, EuiColorPalettePickerPaletteProps } from '@elastic/eui'; +import { palettes, ColorPalette } from '../../../common/lib/palettes'; +import { ComponentStrings } from '../../../i18n'; + +const { PalettePicker: strings } = ComponentStrings; + +interface RequiredProps { + id?: string; + onChange?: (palette: ColorPalette) => void; + palette: ColorPalette; + clearable?: false; +} + +interface ClearableProps { + id?: string; + onChange?: (palette: ColorPalette | null) => void; + palette: ColorPalette | null; + clearable: true; +} + +type Props = RequiredProps | ClearableProps; + +export const PalettePicker: FC = (props) => { + const colorPalettes: EuiColorPalettePickerPaletteProps[] = palettes.map((item) => ({ + value: item.id, + title: item.label, + type: item.gradient ? 'gradient' : 'fixed', + palette: item.colors, + })); + + if (props.clearable) { + const { palette, onChange = () => {} } = props; + + colorPalettes.unshift({ + value: 'clear', + title: strings.getEmptyPaletteLabel(), + type: 'text', + }); + + const onPickerChange = (value: string) => { + const canvasPalette = palettes.find((item) => item.id === value); + onChange(canvasPalette || null); + }; + + return ( + + ); + } + + const { palette, onChange = () => {} } = props; + + const onPickerChange = (value: string) => { + const canvasPalette = palettes.find((item) => item.id === value); + + if (!canvasPalette) { + throw new Error(strings.getNoPaletteFoundErrorTitle()); + } + + onChange(canvasPalette); + }; + + return ( + + ); +}; + +PalettePicker.propTypes = { + id: PropTypes.string, + palette: PropTypes.object, + onChange: PropTypes.func, + clearable: PropTypes.bool, +}; diff --git a/x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.js b/x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.js deleted file mode 100644 index 71d16260e00c7..0000000000000 --- a/x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; - -export const PaletteSwatch = ({ colors, gradient }) => { - let colorBoxes; - - if (!gradient) { - colorBoxes = colors.map((color) => ( -
- )); - } else { - colorBoxes = [ -
, - ]; - } - - return ( -
-
-
{colorBoxes}
-
- ); -}; - -PaletteSwatch.propTypes = { - colors: PropTypes.array, - gradient: PropTypes.bool, -}; diff --git a/x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.scss b/x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.scss deleted file mode 100644 index b57c520a5b07f..0000000000000 --- a/x-pack/plugins/canvas/public/components/palette_swatch/palette_swatch.scss +++ /dev/null @@ -1,35 +0,0 @@ -.canvasPaletteSwatch { - display: inline-block; - position: relative; - height: $euiSizeXS; - width: 100%; - overflow: hidden; - text-align: left; - transform: scaleY(1); - transition: transform $euiAnimSlightResistance $euiAnimSpeedExtraFast; - - .canvasPaletteSwatch__background { - position: absolute; - height: $euiSizeXS; - top: 0; - left: 0; - width: 100%; - transform: scaleY(1); - transition: transform $euiAnimSlightResistance $euiAnimSpeedExtraFast; - } - - .canvasPaletteSwatch__foreground { - position: absolute; - height: 100%; // TODO: No idea why this can't be 25, but it leaves a 1px white spot in the palettePicker if its 25 - top: 0; - left: 0; - white-space: nowrap; - width: 100%; - display: flex; - } - - .canvasPaletteSwatch__box { - display: inline-block; - width: 100%; - } -} diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index 7b4e1271cca1d..78a34a58f5f78 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -39,8 +39,6 @@ @import '../components/loading/loading'; @import '../components/navbar/navbar'; @import '../components/page_manager/page_manager'; -@import '../components/palette_picker/palette_picker'; -@import '../components/palette_swatch/palette_swatch'; @import '../components/positionable/positionable'; @import '../components/rotation_handle/rotation_handle'; @import '../components/shape_preview/shape_preview'; From 3d821af4514f06449b66dc38b5ae8279381d8002 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Wed, 24 Jun 2020 19:32:01 -0400 Subject: [PATCH 18/82] [Ingest Manager] Support registration of server side callbacks for Create Datasource API (#69428) (#69840) * Ingest: Expose `registerExternalCallback()` method out of Ingest server `start` lifecycle * Ingest: Add support for External Callbacks on REST `createDatasourceHandler()` * Ingest: expose DatasourceServices to Plugin start interface * Endpoint: Added Endpoint Ingest handler for Create Datasources - Also moved the temporary logic from the middleware to the handler (still temporary) Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- x-pack/plugins/ingest_manager/server/index.ts | 3 + x-pack/plugins/ingest_manager/server/mocks.ts | 36 ++ .../plugins/ingest_manager/server/plugin.ts | 26 +- .../datasource/datasource_handlers.test.ts | 332 ++++++++++++++++++ .../server/routes/datasource/handlers.ts | 41 ++- .../server/services/app_context.ts | 20 +- .../server/services/datasource.ts | 1 + .../policy/store/policy_details/middleware.ts | 18 - .../endpoint/alerts/handlers/alerts.test.ts | 6 +- .../endpoint/endpoint_app_context_services.ts | 13 +- .../server/endpoint/ingest_integration.ts | 49 +++ .../server/endpoint/mocks.ts | 25 +- .../endpoint/routes/metadata/metadata.test.ts | 17 +- .../endpoint/routes/policy/handlers.test.ts | 12 +- .../security_solution/server/plugin.ts | 2 + 15 files changed, 553 insertions(+), 48 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/mocks.ts create mode 100644 x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index f6b2d7ccc6d48..1e9011c9dfe4f 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -11,6 +11,7 @@ export { IngestManagerSetupContract, IngestManagerSetupDeps, IngestManagerStartContract, + ExternalCallback, } from './plugin'; export const config = { @@ -42,6 +43,8 @@ export const config = { export type IngestManagerConfigType = TypeOf; +export { DatasourceServiceInterface } from './services/datasource'; + export const plugin = (initializerContext: PluginInitializerContext) => { return new IngestManagerPlugin(initializerContext); }; diff --git a/x-pack/plugins/ingest_manager/server/mocks.ts b/x-pack/plugins/ingest_manager/server/mocks.ts new file mode 100644 index 0000000000000..3bdef14dc85a0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/mocks.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { IngestManagerAppContext } from './plugin'; +import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; +import { securityMock } from '../../security/server/mocks'; +import { DatasourceServiceInterface } from './services/datasource'; + +export const createAppContextStartContractMock = (): IngestManagerAppContext => { + return { + encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(), + savedObjects: savedObjectsServiceMock.createStartContract(), + security: securityMock.createSetup(), + logger: loggingSystemMock.create().get(), + isProductionMode: true, + kibanaVersion: '8.0.0', + }; +}; + +export const createDatasourceServiceMock = () => { + return { + assignPackageStream: jest.fn(), + buildDatasourceFromPackage: jest.fn(), + bulkCreate: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + get: jest.fn(), + getByIDs: jest.fn(), + list: jest.fn(), + update: jest.fn(), + } as jest.Mocked; +}; diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index ca13304dcc0dc..6cb448bbfa959 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -45,13 +45,14 @@ import { registerSettingsRoutes, registerAppRoutes, } from './routes'; -import { IngestManagerConfigType } from '../common'; +import { IngestManagerConfigType, NewDatasource } from '../common'; import { appContextService, licenseService, ESIndexPatternSavedObjectService, ESIndexPatternService, AgentService, + datasourceService, } from './services'; import { getAgentStatusById } from './services/agents'; import { CloudSetup } from '../../cloud/server'; @@ -92,12 +93,31 @@ const allSavedObjectTypes = [ ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, ]; +/** + * Callbacks supported by the Ingest plugin + */ +export type ExternalCallback = [ + 'datasourceCreate', + (newDatasource: NewDatasource) => Promise +]; + +export type ExternalCallbacksStorage = Map>; + /** * Describes public IngestManager plugin contract returned at the `startup` stage. */ export interface IngestManagerStartContract { esIndexPatternService: ESIndexPatternService; agentService: AgentService; + /** + * Services for Ingest's Datasources + */ + datasourceService: typeof datasourceService; + /** + * Register callbacks for inclusion in ingest API processing + * @param args + */ + registerExternalCallback: (...args: ExternalCallback) => void; } export class IngestManagerPlugin @@ -237,6 +257,10 @@ export class IngestManagerPlugin agentService: { getAgentStatusById, }, + datasourceService, + registerExternalCallback: (...args: ExternalCallback) => { + return appContextService.addExternalCallback(...args); + }, }; } diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts new file mode 100644 index 0000000000000..07cbeb8b2cec5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts @@ -0,0 +1,332 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServerMock, httpServiceMock } from 'src/core/server/mocks'; +import { IRouter, KibanaRequest, Logger, RequestHandler, RouteConfig } from 'kibana/server'; +import { registerRoutes } from './index'; +import { DATASOURCE_API_ROUTES } from '../../../common/constants'; +import { xpackMocks } from '../../../../../mocks'; +import { appContextService } from '../../services'; +import { createAppContextStartContractMock } from '../../mocks'; +import { DatasourceServiceInterface, ExternalCallback } from '../..'; +import { CreateDatasourceRequestSchema } from '../../types/rest_spec'; +import { datasourceService } from '../../services'; + +const datasourceServiceMock = datasourceService as jest.Mocked; + +jest.mock('../../services/datasource', (): { + datasourceService: jest.Mocked; +} => { + return { + datasourceService: { + assignPackageStream: jest.fn((packageInfo, dataInputs) => Promise.resolve(dataInputs)), + buildDatasourceFromPackage: jest.fn(), + bulkCreate: jest.fn(), + create: jest.fn((soClient, newData) => + Promise.resolve({ + ...newData, + id: '1', + revision: 1, + updated_at: new Date().toISOString(), + updated_by: 'elastic', + created_at: new Date().toISOString(), + created_by: 'elastic', + }) + ), + delete: jest.fn(), + get: jest.fn(), + getByIDs: jest.fn(), + list: jest.fn(), + update: jest.fn(), + }, + }; +}); + +jest.mock('../../services/epm/packages', () => { + return { + ensureInstalledPackage: jest.fn(() => Promise.resolve()), + getPackageInfo: jest.fn(() => Promise.resolve()), + }; +}); + +describe('When calling datasource', () => { + let routerMock: jest.Mocked; + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + let context: ReturnType; + let response: ReturnType; + + beforeAll(() => { + routerMock = httpServiceMock.createRouter(); + registerRoutes(routerMock); + }); + + beforeEach(() => { + appContextService.start(createAppContextStartContractMock()); + context = xpackMocks.createRequestHandlerContext(); + response = httpServerMock.createResponseFactory(); + }); + + afterEach(() => { + jest.clearAllMocks(); + appContextService.stop(); + }); + + describe('create api handler', () => { + const getCreateKibanaRequest = ( + newData?: typeof CreateDatasourceRequestSchema.body + ): KibanaRequest => { + return httpServerMock.createKibanaRequest< + undefined, + undefined, + typeof CreateDatasourceRequestSchema.body + >({ + path: routeConfig.path, + method: 'post', + body: newData || { + name: 'endpoint-1', + description: '', + config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', + enabled: true, + output_id: '', + inputs: [], + namespace: 'default', + package: { name: 'endpoint', title: 'Elastic Endpoint', version: '0.5.0' }, + }, + }); + }; + + // Set the routeConfig and routeHandler to the Create API + beforeAll(() => { + [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => + path.startsWith(DATASOURCE_API_ROUTES.CREATE_PATTERN) + )!; + }); + + describe('and external callbacks are registered', () => { + const callbackCallingOrder: string[] = []; + + // Callback one adds an input that includes a `config` property + const callbackOne: ExternalCallback[1] = jest.fn(async (ds) => { + callbackCallingOrder.push('one'); + const newDs = { + ...ds, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + one: { + value: 'inserted by callbackOne', + }, + }, + }, + ], + }; + return newDs; + }); + + // Callback two adds an additional `input[0].config` property + const callbackTwo: ExternalCallback[1] = jest.fn(async (ds) => { + callbackCallingOrder.push('two'); + const newDs = { + ...ds, + inputs: [ + { + ...ds.inputs[0], + config: { + ...ds.inputs[0].config, + two: { + value: 'inserted by callbackTwo', + }, + }, + }, + ], + }; + return newDs; + }); + + beforeEach(() => { + appContextService.addExternalCallback('datasourceCreate', callbackOne); + appContextService.addExternalCallback('datasourceCreate', callbackTwo); + }); + + afterEach(() => (callbackCallingOrder.length = 0)); + + it('should call external callbacks in expected order', async () => { + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalled(); + expect(callbackCallingOrder).toEqual(['one', 'two']); + }); + + it('should feed datasource returned by last callback', async () => { + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalled(); + expect(callbackOne).toHaveBeenCalledWith({ + config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', + description: '', + enabled: true, + inputs: [], + name: 'endpoint-1', + namespace: 'default', + output_id: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + }, + }); + expect(callbackTwo).toHaveBeenCalledWith({ + config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', + description: '', + enabled: true, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + one: { + value: 'inserted by callbackOne', + }, + }, + }, + ], + name: 'endpoint-1', + namespace: 'default', + output_id: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + }, + }); + }); + + it('should create with data from callback', async () => { + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalled(); + expect(datasourceServiceMock.create.mock.calls[0][1]).toEqual({ + config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', + description: '', + enabled: true, + inputs: [ + { + config: { + one: { + value: 'inserted by callbackOne', + }, + two: { + value: 'inserted by callbackTwo', + }, + }, + enabled: true, + streams: [], + type: 'endpoint', + }, + ], + name: 'endpoint-1', + namespace: 'default', + output_id: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + }, + }); + }); + + describe('and a callback throws an exception', () => { + const callbackThree: ExternalCallback[1] = jest.fn(async (ds) => { + callbackCallingOrder.push('three'); + throw new Error('callbackThree threw error on purpose'); + }); + + const callbackFour: ExternalCallback[1] = jest.fn(async (ds) => { + callbackCallingOrder.push('four'); + return { + ...ds, + inputs: [ + { + ...ds.inputs[0], + config: { + ...ds.inputs[0].config, + four: { + value: 'inserted by callbackFour', + }, + }, + }, + ], + }; + }); + + beforeEach(() => { + appContextService.addExternalCallback('datasourceCreate', callbackThree); + appContextService.addExternalCallback('datasourceCreate', callbackFour); + }); + + it('should skip over callback exceptions and still execute other callbacks', async () => { + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalled(); + expect(callbackCallingOrder).toEqual(['one', 'two', 'three', 'four']); + }); + + it('should log errors', async () => { + const errorLogger = (appContextService.getLogger() as jest.Mocked).error; + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalled(); + expect(errorLogger.mock.calls).toEqual([ + ['An external registered [datasourceCreate] callback failed when executed'], + [new Error('callbackThree threw error on purpose')], + ]); + }); + + it('should create datasource with last successful returned datasource', async () => { + const request = getCreateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalled(); + expect(datasourceServiceMock.create.mock.calls[0][1]).toEqual({ + config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', + description: '', + enabled: true, + inputs: [ + { + config: { + one: { + value: 'inserted by callbackOne', + }, + two: { + value: 'inserted by callbackTwo', + }, + four: { + value: 'inserted by callbackFour', + }, + }, + enabled: true, + streams: [], + type: 'endpoint', + }, + ], + name: 'endpoint-1', + namespace: 'default', + output_id: '', + package: { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + }, + }); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts index 09daec3370400..4f83d24a846ea 100644 --- a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts @@ -14,6 +14,7 @@ import { CreateDatasourceRequestSchema, UpdateDatasourceRequestSchema, DeleteDatasourcesRequestSchema, + NewDatasource, } from '../../types'; import { CreateDatasourceResponse, DeleteDatasourcesResponse } from '../../../common'; @@ -76,23 +77,50 @@ export const createDatasourceHandler: RequestHandler< const soClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; - const newData = { ...request.body }; + const logger = appContextService.getLogger(); + let newData = { ...request.body }; try { + // If we have external callbacks, then process those now before creating the actual datasource + const externalCallbacks = appContextService.getExternalCallbacks('datasourceCreate'); + if (externalCallbacks && externalCallbacks.size > 0) { + let updatedNewData: NewDatasource = newData; + + for (const callback of externalCallbacks) { + try { + // ensure that the returned value by the callback passes schema validation + updatedNewData = CreateDatasourceRequestSchema.body.validate( + await callback(updatedNewData) + ); + } catch (error) { + // Log the error, but keep going and process the other callbacks + logger.error('An external registered [datasourceCreate] callback failed when executed'); + logger.error(error); + } + } + + // The type `NewDatasource` and the `DatasourceBaseSchema` are incompatible. + // `NewDatasrouce` defines `namespace` as optional string, which means that `undefined` is a + // valid value, however, the schema defines it as string with a minimum length of 1. + // Here, we need to cast the value back to the schema type and ignore the TS error. + // @ts-ignore + newData = updatedNewData as typeof CreateDatasourceRequestSchema.body; + } + // Make sure the datasource package is installed - if (request.body.package?.name) { + if (newData.package?.name) { await ensureInstalledPackage({ savedObjectsClient: soClient, - pkgName: request.body.package.name, + pkgName: newData.package.name, callCluster, }); const pkgInfo = await getPackageInfo({ savedObjectsClient: soClient, - pkgName: request.body.package.name, - pkgVersion: request.body.package.version, + pkgName: newData.package.name, + pkgVersion: newData.package.version, }); newData.inputs = (await datasourceService.assignPackageStream( pkgInfo, - request.body.inputs + newData.inputs )) as TypeOf['inputs']; } @@ -103,6 +131,7 @@ export const createDatasourceHandler: RequestHandler< body, }); } catch (e) { + logger.error(e); return response.customError({ statusCode: 500, body: { message: e.message }, diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index 5ed6f7c5e54d1..4d109b73d12d9 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -12,7 +12,7 @@ import { } from '../../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../../security/server'; import { IngestManagerConfigType } from '../../common'; -import { IngestManagerAppContext } from '../plugin'; +import { ExternalCallback, ExternalCallbacksStorage, IngestManagerAppContext } from '../plugin'; import { CloudSetup } from '../../../cloud/server'; class AppContextService { @@ -27,6 +27,7 @@ class AppContextService { private cloud?: CloudSetup; private logger: Logger | undefined; private httpSetup?: HttpServiceSetup; + private externalCallbacks: ExternalCallbacksStorage = new Map(); public async start(appContext: IngestManagerAppContext) { this.encryptedSavedObjects = appContext.encryptedSavedObjectsStart?.getClient(); @@ -47,7 +48,9 @@ class AppContextService { } } - public stop() {} + public stop() { + this.externalCallbacks.clear(); + } public getEncryptedSavedObjects() { if (!this.encryptedSavedObjects) { @@ -121,6 +124,19 @@ class AppContextService { } return this.kibanaVersion; } + + public addExternalCallback(type: ExternalCallback[0], callback: ExternalCallback[1]) { + if (!this.externalCallbacks.has(type)) { + this.externalCallbacks.set(type, new Set()); + } + this.externalCallbacks.get(type)!.add(callback); + } + + public getExternalCallbacks(type: ExternalCallback[0]) { + if (this.externalCallbacks) { + return this.externalCallbacks.get(type); + } + } } export const appContextService = new AppContextService(); diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts index 3ad94ea8191d4..f3f460d2a7420 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.ts @@ -307,4 +307,5 @@ async function _assignPackageStreamToStream( return { ...stream }; } +export type DatasourceServiceInterface = DatasourceService; export const datasourceService = new DatasourceService(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts index ec0c526482b45..899f85ecdea30 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts @@ -17,7 +17,6 @@ import { sendPutDatasource, } from '../policy_list/services/ingest'; import { NewPolicyData, PolicyData } from '../../../../../../common/endpoint/types'; -import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config'; import { ImmutableMiddlewareFactory } from '../../../../../common/store'; export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory = ( @@ -43,23 +42,6 @@ export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory { routerMock = httpServiceMock.createRouter(); endpointAppContextService = new EndpointAppContextService(); - endpointAppContextService.start({ - agentService: createMockAgentService(), - }); + endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); registerAlertRoutes(routerMock, { logFactory: loggingSystemMock.create(), diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index cb8c913a73b8e..7b8a368b6c975 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -3,7 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { AgentService } from '../../../ingest_manager/server'; +import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server'; +import { handleDatasourceCreate } from './ingest_integration'; + +export type EndpointAppContextServiceStartContract = Pick< + IngestManagerStartContract, + 'agentService' +> & { + registerIngestCallback: IngestManagerStartContract['registerExternalCallback']; +}; /** * A singleton that holds shared services that are initialized during the start up phase @@ -12,8 +20,9 @@ import { AgentService } from '../../../ingest_manager/server'; export class EndpointAppContextService { private agentService: AgentService | undefined; - public start(dependencies: { agentService: AgentService }) { + public start(dependencies: EndpointAppContextServiceStartContract) { this.agentService = dependencies.agentService; + dependencies.registerIngestCallback('datasourceCreate', handleDatasourceCreate); } public stop() {} diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts new file mode 100644 index 0000000000000..6ff0949311587 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; +import { NewPolicyData } from '../../common/endpoint/types'; +import { NewDatasource } from '../../../ingest_manager/common/types/models'; + +/** + * Callback to handle creation of Datasources in Ingest Manager + * @param newDatasource + */ +export const handleDatasourceCreate = async ( + newDatasource: NewDatasource +): Promise => { + // We only care about Endpoint datasources + if (newDatasource.package?.name !== 'endpoint') { + return newDatasource; + } + + // We cast the type here so that any changes to the Endpoint specific data + // follow the types/schema expected + let updatedDatasource = newDatasource as NewPolicyData; + + // Until we get the Default Policy Configuration in the Endpoint package, + // we will add it here manually at creation time. + // @ts-ignore + if (newDatasource.inputs.length === 0) { + updatedDatasource = { + ...newDatasource, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { + value: policyConfigFactory(), + }, + }, + }, + ], + }; + } + + return updatedDatasource; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index b10e9e4dc90e7..5435eff4ef150 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -6,7 +6,28 @@ import { IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; import { xpackMocks } from '../../../../mocks'; -import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server'; +import { + AgentService, + IngestManagerStartContract, + ExternalCallback, +} from '../../../ingest_manager/server'; +import { EndpointAppContextServiceStartContract } from './endpoint_app_context_services'; +import { createDatasourceServiceMock } from '../../../ingest_manager/server/mocks'; + +/** + * Crates a mocked input contract for the `EndpointAppContextService#start()` method + */ +export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< + EndpointAppContextServiceStartContract +> => { + return { + agentService: createMockAgentService(), + registerIngestCallback: jest.fn< + ReturnType, + Parameters + >(), + }; +}; /** * Creates a mock AgentService @@ -32,6 +53,8 @@ export const createMockIngestManagerStartContract = ( getESIndexPattern: jest.fn().mockResolvedValue(indexPattern), }, agentService: createMockAgentService(), + registerExternalCallback: jest.fn((...args: ExternalCallback) => {}), + datasourceService: createDatasourceServiceMock(), }; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index ba51a3b6aa92e..c04975fa8b28e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -27,8 +27,10 @@ import { } from '../../../../common/endpoint/types'; import { SearchResponse } from 'elasticsearch'; import { registerEndpointRoutes } from './index'; -import { createMockAgentService, createRouteHandlerContext } from '../../mocks'; -import { AgentService } from '../../../../../ingest_manager/server'; +import { + createMockEndpointAppContextServiceStartContract, + createRouteHandlerContext, +} from '../../mocks'; import Boom from 'boom'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; @@ -44,7 +46,9 @@ describe('test endpoint route', () => { let routeHandler: RequestHandler; // eslint-disable-next-line @typescript-eslint/no-explicit-any let routeConfig: RouteConfig; - let mockAgentService: jest.Mocked; + let mockAgentService: ReturnType< + typeof createMockEndpointAppContextServiceStartContract + >['agentService']; let endpointAppContextService: EndpointAppContextService; beforeEach(() => { @@ -56,11 +60,10 @@ describe('test endpoint route', () => { mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); mockResponse = httpServerMock.createResponseFactory(); - mockAgentService = createMockAgentService(); endpointAppContextService = new EndpointAppContextService(); - endpointAppContextService.start({ - agentService: mockAgentService, - }); + const startContract = createMockEndpointAppContextServiceStartContract(); + endpointAppContextService.start(startContract); + mockAgentService = startContract.agentService; registerEndpointRoutes(routerMock, { logFactory: loggingSystemMock.create(), diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index 6c1f0a206ffaa..16af3a95bc72d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { EndpointAppContextService } from '../../endpoint_app_context_services'; -import { createMockAgentService, createRouteHandlerContext } from '../../mocks'; +import { + createMockEndpointAppContextServiceStartContract, + createRouteHandlerContext, +} from '../../mocks'; import { getHostPolicyResponseHandler } from './handlers'; import { IScopedClusterClient, @@ -17,7 +20,6 @@ import { loggingSystemMock, savedObjectsClientMock, } from '../../../../../../../src/core/server/mocks'; -import { AgentService } from '../../../../../ingest_manager/server/services'; import { SearchResponse } from 'elasticsearch'; import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; @@ -28,17 +30,13 @@ describe('test policy response handler', () => { let mockScopedClient: jest.Mocked; let mockSavedObjectClient: jest.Mocked; let mockResponse: jest.Mocked; - let mockAgentService: jest.Mocked; beforeEach(() => { mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); mockSavedObjectClient = savedObjectsClientMock.create(); mockResponse = httpServerMock.createResponseFactory(); endpointAppContextService = new EndpointAppContextService(); - mockAgentService = createMockAgentService(); - endpointAppContextService.start({ - agentService: mockAgentService, - }); + endpointAppContextService.start(createMockEndpointAppContextServiceStartContract()); }); afterEach(() => endpointAppContextService.stop()); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 9fe7307e8cb6d..879c132ddec54 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -219,7 +219,9 @@ export class Plugin implements IPlugin Date: Wed, 24 Jun 2020 20:33:58 -0400 Subject: [PATCH 19/82] [IngestManager] Expose agent authentication using access key (#69650) (#69850) * [IngestManager] Expose agent authentication using access key * Add unit tests to authenticateAgentWithAccessToken service Co-authored-by: Nicolas Chaulet --- .../plugins/ingest_manager/server/plugin.ts | 3 +- .../server/routes/agent/acks_handlers.test.ts | 2 +- .../server/routes/agent/acks_handlers.ts | 4 +- .../server/routes/agent/handlers.ts | 3 +- .../server/routes/agent/index.ts | 2 +- .../server/services/agents/acks.ts | 4 +- .../services/agents/authenticate.test.ts | 154 ++++++++++++++++++ .../server/services/agents/authenticate.ts | 30 ++++ .../server/services/agents/index.ts | 1 + 9 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/agents/authenticate.test.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/agents/authenticate.ts diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 6cb448bbfa959..89f3c651eb71a 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -54,7 +54,7 @@ import { AgentService, datasourceService, } from './services'; -import { getAgentStatusById } from './services/agents'; +import { getAgentStatusById, authenticateAgentWithAccessToken } from './services/agents'; import { CloudSetup } from '../../cloud/server'; import { agentCheckinState } from './services/agents/checkin/state'; @@ -256,6 +256,7 @@ export class IngestManagerPlugin esIndexPatternService: new ESIndexPatternSavedObjectService(), agentService: { getAgentStatusById, + authenticateAgentWithAccessToken, }, datasourceService, registerExternalCallback: (...args: ExternalCallback) => { diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts index 84923d5c33664..aaed189ae3ddd 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts @@ -77,7 +77,7 @@ describe('test acks handlers', () => { id: 'action1', }, ]), - getAgentByAccessAPIKeyId: jest.fn().mockReturnValueOnce({ + authenticateAgentWithAccessToken: jest.fn().mockReturnValueOnce({ id: 'agent', }), getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient), diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts index 83d894295c312..0b719d8a67df7 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts @@ -9,7 +9,6 @@ import { RequestHandler } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { PostAgentAcksRequestSchema } from '../../types/rest_spec'; -import * as APIKeyService from '../../services/api_keys'; import { AcksService } from '../../services/agents'; import { AgentEvent } from '../../../common/types/models'; import { PostAgentAcksResponse } from '../../../common/types/rest_spec'; @@ -24,8 +23,7 @@ export const postAgentAcksHandlerBuilder = function ( return async (context, request, response) => { try { const soClient = ackService.getSavedObjectsClientContract(request); - const res = APIKeyService.parseApiKeyFromHeaders(request.headers); - const agent = await ackService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string); + const agent = await ackService.authenticateAgentWithAccessToken(soClient, request); const agentEvents = request.body.events as AgentEvent[]; // validate that all events are for the authorized agent obtained from the api key diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 0d1c77b8d697f..d31498599a2b6 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -171,8 +171,7 @@ export const postAgentCheckinHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = appContextService.getInternalUserSOClient(request); - const res = APIKeyService.parseApiKeyFromHeaders(request.headers); - const agent = await AgentService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId); + const agent = await AgentService.authenticateAgentWithAccessToken(soClient, request); const abortController = new AbortController(); request.events.aborted$.subscribe(() => { abortController.abort(); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index 87eee4622c80b..eaab46c7b455c 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -109,7 +109,7 @@ export const registerRoutes = (router: IRouter) => { }, postAgentAcksHandlerBuilder({ acknowledgeAgentActions: AgentService.acknowledgeAgentActions, - getAgentByAccessAPIKeyId: AgentService.getAgentByAccessAPIKeyId, + authenticateAgentWithAccessToken: AgentService.authenticateAgentWithAccessToken, getSavedObjectsClientContract: appContextService.getInternalUserSOClient.bind( appContextService ), diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts index 81ba9754e8aa4..a1b48a879bb89 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts @@ -140,9 +140,9 @@ export interface AcksService { actionIds: AgentEvent[] ) => Promise; - getAgentByAccessAPIKeyId: ( + authenticateAgentWithAccessToken: ( soClient: SavedObjectsClientContract, - accessAPIKeyId: string + request: KibanaRequest ) => Promise; getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/authenticate.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/authenticate.test.ts new file mode 100644 index 0000000000000..b56ca4ca8cc17 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/authenticate.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from 'kibana/server'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; + +import { authenticateAgentWithAccessToken } from './authenticate'; + +describe('test agent autenticate services', () => { + it('should succeed with a valid API key and an active agent', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.find.mockReturnValue( + Promise.resolve({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + id: 'agent1', + type: 'agent', + references: [], + score: 0, + attributes: { + active: true, + access_api_key_id: 'pedTuHIBTEDt93wW0Fhr', + }, + }, + ], + }) + ); + await authenticateAgentWithAccessToken(mockSavedObjectsClient, { + auth: { isAuthenticated: true }, + headers: { + authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', + }, + } as KibanaRequest); + }); + + it('should throw if the request is not authenticated', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.find.mockReturnValue( + Promise.resolve({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + id: 'agent1', + type: 'agent', + references: [], + score: 0, + attributes: { + active: true, + access_api_key_id: 'pedTuHIBTEDt93wW0Fhr', + }, + }, + ], + }) + ); + expect( + authenticateAgentWithAccessToken(mockSavedObjectsClient, { + auth: { isAuthenticated: false }, + headers: { + authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', + }, + } as KibanaRequest) + ).rejects.toThrow(/Request not authenticated/); + }); + + it('should throw if the ApiKey headers is malformed', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.find.mockReturnValue( + Promise.resolve({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + id: 'agent1', + type: 'agent', + references: [], + score: 0, + attributes: { + active: false, + access_api_key_id: 'pedTuHIBTEDt93wW0Fhr', + }, + }, + ], + }) + ); + expect( + authenticateAgentWithAccessToken(mockSavedObjectsClient, { + auth: { isAuthenticated: true }, + headers: { + authorization: 'aaaa', + }, + } as KibanaRequest) + ).rejects.toThrow(/Authorization header is malformed/); + }); + + it('should throw if the agent is not active', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.find.mockReturnValue( + Promise.resolve({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + id: 'agent1', + type: 'agent', + references: [], + score: 0, + attributes: { + active: false, + access_api_key_id: 'pedTuHIBTEDt93wW0Fhr', + }, + }, + ], + }) + ); + expect( + authenticateAgentWithAccessToken(mockSavedObjectsClient, { + auth: { isAuthenticated: true }, + headers: { + authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', + }, + } as KibanaRequest) + ).rejects.toThrow(/Agent inactive/); + }); + + it('should throw if there is no agent matching the API key', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.find.mockReturnValue( + Promise.resolve({ + page: 1, + per_page: 100, + total: 1, + saved_objects: [], + }) + ); + expect( + authenticateAgentWithAccessToken(mockSavedObjectsClient, { + auth: { isAuthenticated: true }, + headers: { + authorization: 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw==', + }, + } as KibanaRequest) + ).rejects.toThrow(/Agent not found/); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/authenticate.ts b/x-pack/plugins/ingest_manager/server/services/agents/authenticate.ts new file mode 100644 index 0000000000000..2515a02da4e78 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/authenticate.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; +import { Agent } from '../../types'; +import * as APIKeyService from '../api_keys'; +import { getAgentByAccessAPIKeyId } from './crud'; + +export async function authenticateAgentWithAccessToken( + soClient: SavedObjectsClientContract, + request: KibanaRequest +): Promise { + if (!request.auth.isAuthenticated) { + throw Boom.unauthorized('Request not authenticated'); + } + let res: { apiKey: string; apiKeyId: string }; + try { + res = APIKeyService.parseApiKeyFromHeaders(request.headers); + } catch (err) { + throw Boom.unauthorized(err.message); + } + + const agent = await getAgentByAccessAPIKeyId(soClient, res.apiKeyId); + + return agent; +} diff --git a/x-pack/plugins/ingest_manager/server/services/agents/index.ts b/x-pack/plugins/ingest_manager/server/services/agents/index.ts index 257091af0ebd0..400c099af4e93 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/index.ts @@ -14,3 +14,4 @@ export * from './crud'; export * from './update'; export * from './actions'; export * from './reassign'; +export * from './authenticate'; From 8c27e763d77c5e8be7ff63ef269e20f46ef398c5 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 24 Jun 2020 21:36:40 -0700 Subject: [PATCH 20/82] Add delete data stream action and detail panel (#68919) (#69873) Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../helpers/http_requests.ts | 18 ++ .../helpers/setup_environment.tsx | 18 +- .../home/data_streams_tab.helpers.ts | 116 ++++++++-- .../home/data_streams_tab.test.ts | 200 +++++++++++++----- .../home/indices_tab.helpers.ts | 15 ++ .../home/indices_tab.test.ts | 21 +- .../common/lib/data_stream_serialization.ts | 12 +- .../index_management/common/lib/index.ts | 2 +- x-pack/plugins/index_management/kibana.json | 3 +- .../public/application/app_context.tsx | 6 +- .../application/mount_management_section.ts | 5 +- .../data_stream_detail_panel.tsx | 136 ++++++++---- .../data_stream_list/data_stream_list.tsx | 107 +++++++--- .../data_stream_table/data_stream_table.tsx | 86 ++++++-- .../delete_data_stream_confirmation_modal.tsx | 149 +++++++++++++ .../index.ts | 7 + .../public/application/services/api.ts | 13 +- .../plugins/index_management/public/plugin.ts | 7 +- .../server/client/elasticsearch.ts | 14 ++ .../server/routes/api/data_streams/index.ts | 5 +- .../api/data_streams/register_delete_route.ts | 52 +++++ .../api/data_streams/register_get_route.ts | 41 +++- x-pack/plugins/ingest_manager/public/index.ts | 2 +- .../plugins/ingest_manager/public/plugin.ts | 8 +- 24 files changed, 860 insertions(+), 183 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx create mode 100644 x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/index.ts create mode 100644 x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index 56d76da522ac2..907c749f8ec0b 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -35,6 +35,22 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setLoadDataStreamResponse = (response: HttpResponse = []) => { + server.respondWith('GET', `${API_BASE_PATH}/data_streams/:id`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(response), + ]); + }; + + const setDeleteDataStreamResponse = (response: HttpResponse = []) => { + server.respondWith('POST', `${API_BASE_PATH}/delete_data_streams`, [ + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(response), + ]); + }; + const setDeleteTemplateResponse = (response: HttpResponse = []) => { server.respondWith('POST', `${API_BASE_PATH}/delete_index_templates`, [ 200, @@ -80,6 +96,8 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { setLoadTemplatesResponse, setLoadIndicesResponse, setLoadDataStreamsResponse, + setLoadDataStreamResponse, + setDeleteDataStreamResponse, setDeleteTemplateResponse, setLoadTemplateResponse, setCreateTemplateResponse, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx index 0a49191fdb149..d85db94d4a970 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx @@ -8,6 +8,7 @@ import React from 'react'; import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; +import { merge } from 'lodash'; import { notificationServiceMock, @@ -33,7 +34,7 @@ export const services = { services.uiMetricService.setup({ reportUiStats() {} } as any); setExtensionsService(services.extensionsService); setUiMetricService(services.uiMetricService); -const appDependencies = { services, core: {}, plugins: {} } as any; +const appDependencies = { services, core: { getUrlForApp: () => {} }, plugins: {} } as any; export const setupEnvironment = () => { // Mock initialization of services @@ -51,8 +52,13 @@ export const setupEnvironment = () => { }; }; -export const WithAppDependencies = (Comp: any) => (props: any) => ( - - - -); +export const WithAppDependencies = (Comp: any, overridingDependencies: any = {}) => ( + props: any +) => { + const mergedDependencies = merge({}, appDependencies, overridingDependencies); + return ( + + + + ); +}; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts index 572889954db6a..ecea230ecab85 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts @@ -5,6 +5,7 @@ */ import { act } from 'react-dom/test-utils'; +import { ReactWrapper } from 'enzyme'; import { registerTestBed, @@ -17,27 +18,38 @@ import { IndexManagementHome } from '../../../public/application/sections/home'; import { indexManagementStore } from '../../../public/application/store'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { WithAppDependencies, services, TestSubjects } from '../helpers'; -const testBedConfig: TestBedConfig = { - store: () => indexManagementStore(services as any), - memoryRouter: { - initialEntries: [`/indices`], - componentRoutePath: `/:section(indices|data_streams|templates)`, - }, - doMountAsync: true, -}; - -const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), testBedConfig); - export interface DataStreamsTabTestBed extends TestBed { actions: { goToDataStreamsList: () => void; clickEmptyPromptIndexTemplateLink: () => void; clickReloadButton: () => void; + clickNameAt: (index: number) => void; clickIndicesAt: (index: number) => void; + clickDeletActionAt: (index: number) => void; + clickConfirmDelete: () => void; + clickDeletDataStreamButton: () => void; }; + findDeleteActionAt: (index: number) => ReactWrapper; + findDeleteConfirmationModal: () => ReactWrapper; + findDetailPanel: () => ReactWrapper; + findDetailPanelTitle: () => string; + findEmptyPromptIndexTemplateLink: () => ReactWrapper; } -export const setup = async (): Promise => { +export const setup = async (overridingDependencies: any = {}): Promise => { + const testBedConfig: TestBedConfig = { + store: () => indexManagementStore(services as any), + memoryRouter: { + initialEntries: [`/indices`], + componentRoutePath: `/:section(indices|data_streams|templates)`, + }, + doMountAsync: true, + }; + + const initTestBed = registerTestBed( + WithAppDependencies(IndexManagementHome, overridingDependencies), + testBedConfig + ); const testBed = await initTestBed(); /** @@ -48,15 +60,17 @@ export const setup = async (): Promise => { testBed.find('data_streamsTab').simulate('click'); }; - const clickEmptyPromptIndexTemplateLink = async () => { - const { find, component, router } = testBed; - + const findEmptyPromptIndexTemplateLink = () => { + const { find } = testBed; const templateLink = find('dataStreamsEmptyPromptTemplateLink'); + return templateLink; + }; + const clickEmptyPromptIndexTemplateLink = async () => { + const { component, router } = testBed; await act(async () => { - router.navigateTo(templateLink.props().href!); + router.navigateTo(findEmptyPromptIndexTemplateLink().props().href!); }); - component.update(); }; @@ -65,10 +79,15 @@ export const setup = async (): Promise => { find('reloadButton').simulate('click'); }; - const clickIndicesAt = async (index: number) => { - const { component, table, router } = testBed; + const findTestSubjectAt = (testSubject: string, index: number) => { + const { table } = testBed; const { rows } = table.getMetaData('dataStreamTable'); - const indicesLink = findTestSubject(rows[index].reactWrapper, 'indicesLink'); + return findTestSubject(rows[index].reactWrapper, testSubject); + }; + + const clickIndicesAt = async (index: number) => { + const { component, router } = testBed; + const indicesLink = findTestSubjectAt('indicesLink', index); await act(async () => { router.navigateTo(indicesLink.props().href!); @@ -77,14 +96,71 @@ export const setup = async (): Promise => { component.update(); }; + const clickNameAt = async (index: number) => { + const { component, router } = testBed; + const nameLink = findTestSubjectAt('nameLink', index); + + await act(async () => { + router.navigateTo(nameLink.props().href!); + }); + + component.update(); + }; + + const findDeleteActionAt = findTestSubjectAt.bind(null, 'deleteDataStream'); + + const clickDeletActionAt = (index: number) => { + findDeleteActionAt(index).simulate('click'); + }; + + const findDeleteConfirmationModal = () => { + const { find } = testBed; + return find('deleteDataStreamsConfirmation'); + }; + + const clickConfirmDelete = async () => { + const modal = document.body.querySelector('[data-test-subj="deleteDataStreamsConfirmation"]'); + const confirmButton: HTMLButtonElement | null = modal!.querySelector( + '[data-test-subj="confirmModalConfirmButton"]' + ); + + await act(async () => { + confirmButton!.click(); + }); + }; + + const clickDeletDataStreamButton = () => { + const { find } = testBed; + find('deleteDataStreamButton').simulate('click'); + }; + + const findDetailPanel = () => { + const { find } = testBed; + return find('dataStreamDetailPanel'); + }; + + const findDetailPanelTitle = () => { + const { find } = testBed; + return find('dataStreamDetailPanelTitle').text(); + }; + return { ...testBed, actions: { goToDataStreamsList, clickEmptyPromptIndexTemplateLink, clickReloadButton, + clickNameAt, clickIndicesAt, + clickDeletActionAt, + clickConfirmDelete, + clickDeletDataStreamButton, }, + findDeleteActionAt, + findDeleteConfirmationModal, + findDetailPanel, + findDetailPanelTitle, + findEmptyPromptIndexTemplateLink, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts index efe2e2d0c74ae..dfcbb51869466 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts @@ -19,61 +19,38 @@ describe('Data Streams tab', () => { server.restore(); }); - beforeEach(async () => { - httpRequestsMockHelpers.setLoadIndicesResponse([ - { - health: '', - status: '', - primary: '', - replica: '', - documents: '', - documents_deleted: '', - size: '', - primary_size: '', - name: 'data-stream-index', - data_stream: 'dataStream1', - }, - { - health: 'green', - status: 'open', - primary: 1, - replica: 1, - documents: 10000, - documents_deleted: 100, - size: '156kb', - primary_size: '156kb', - name: 'non-data-stream-index', - }, - ]); - - await act(async () => { - testBed = await setup(); - }); - }); - describe('when there are no data streams', () => { beforeEach(async () => { - const { actions, component } = testBed; - + httpRequestsMockHelpers.setLoadIndicesResponse([]); httpRequestsMockHelpers.setLoadDataStreamsResponse([]); httpRequestsMockHelpers.setLoadTemplatesResponse({ templates: [], legacyTemplates: [] }); + }); + + test('displays an empty prompt', async () => { + testBed = await setup(); await act(async () => { - actions.goToDataStreamsList(); + testBed.actions.goToDataStreamsList(); }); + const { exists, component } = testBed; component.update(); - }); - - test('displays an empty prompt', async () => { - const { exists } = testBed; expect(exists('sectionLoading')).toBe(false); expect(exists('emptyPrompt')).toBe(true); }); - test('goes to index templates tab when "Get started" link is clicked', async () => { - const { actions, exists } = testBed; + test('when Ingest Manager is disabled, goes to index templates tab when "Get started" link is clicked', async () => { + testBed = await setup({ + plugins: {}, + }); + + await act(async () => { + testBed.actions.goToDataStreamsList(); + }); + + const { actions, exists, component } = testBed; + component.update(); await act(async () => { actions.clickEmptyPromptIndexTemplateLink(); @@ -81,32 +58,77 @@ describe('Data Streams tab', () => { expect(exists('templateList')).toBe(true); }); + + test('when Ingest Manager is enabled, links to Ingest Manager', async () => { + testBed = await setup({ + plugins: { ingestManager: { hi: 'ok' } }, + }); + + await act(async () => { + testBed.actions.goToDataStreamsList(); + }); + + const { findEmptyPromptIndexTemplateLink, component } = testBed; + component.update(); + + // Assert against the text because the href won't be available, due to dependency upon our core mock. + expect(findEmptyPromptIndexTemplateLink().text()).toBe('Ingest Manager'); + }); }); describe('when there are data streams', () => { beforeEach(async () => { - const { actions, component } = testBed; + httpRequestsMockHelpers.setLoadIndicesResponse([ + { + health: '', + status: '', + primary: '', + replica: '', + documents: '', + documents_deleted: '', + size: '', + primary_size: '', + name: 'data-stream-index', + data_stream: 'dataStream1', + }, + { + health: 'green', + status: 'open', + primary: 1, + replica: 1, + documents: 10000, + documents_deleted: 100, + size: '156kb', + primary_size: '156kb', + name: 'non-data-stream-index', + }, + ]); + + const dataStreamForDetailPanel = createDataStreamPayload('dataStream1'); httpRequestsMockHelpers.setLoadDataStreamsResponse([ - createDataStreamPayload('dataStream1'), + dataStreamForDetailPanel, createDataStreamPayload('dataStream2'), ]); + httpRequestsMockHelpers.setLoadDataStreamResponse(dataStreamForDetailPanel); + + testBed = await setup(); + await act(async () => { - actions.goToDataStreamsList(); + testBed.actions.goToDataStreamsList(); }); - component.update(); + testBed.component.update(); }); test('lists them in the table', async () => { const { table } = testBed; - const { tableCellsValues } = table.getMetaData('dataStreamTable'); expect(tableCellsValues).toEqual([ - ['dataStream1', '1', '@timestamp', '1'], - ['dataStream2', '1', '@timestamp', '1'], + ['', 'dataStream1', '1', ''], + ['', 'dataStream2', '1', ''], ]); }); @@ -126,12 +148,90 @@ describe('Data Streams tab', () => { test('clicking the indices count navigates to the backing indices', async () => { const { table, actions } = testBed; - await actions.clickIndicesAt(0); - expect(table.getMetaData('indexTable').tableCellsValues).toEqual([ ['', '', '', '', '', '', '', 'dataStream1'], ]); }); + + describe('row actions', () => { + test('can delete', () => { + const { findDeleteActionAt } = testBed; + const deleteAction = findDeleteActionAt(0); + expect(deleteAction.length).toBe(1); + }); + }); + + describe('deleting a data stream', () => { + test('shows a confirmation modal', async () => { + const { + actions: { clickDeletActionAt }, + findDeleteConfirmationModal, + } = testBed; + clickDeletActionAt(0); + const confirmationModal = findDeleteConfirmationModal(); + expect(confirmationModal).toBeDefined(); + }); + + test('sends a request to the Delete API', async () => { + const { + actions: { clickDeletActionAt, clickConfirmDelete }, + } = testBed; + clickDeletActionAt(0); + + httpRequestsMockHelpers.setDeleteDataStreamResponse({ + results: { + dataStreamsDeleted: ['dataStream1'], + errors: [], + }, + }); + + await clickConfirmDelete(); + + const { method, url, requestBody } = server.requests[server.requests.length - 1]; + + expect(method).toBe('POST'); + expect(url).toBe(`${API_BASE_PATH}/delete_data_streams`); + expect(JSON.parse(JSON.parse(requestBody).body)).toEqual({ + dataStreams: ['dataStream1'], + }); + }); + }); + + describe('detail panel', () => { + test('opens when the data stream name in the table is clicked', async () => { + const { actions, findDetailPanel, findDetailPanelTitle } = testBed; + await actions.clickNameAt(0); + expect(findDetailPanel().length).toBe(1); + expect(findDetailPanelTitle()).toBe('dataStream1'); + }); + + test('deletes the data stream when delete button is clicked', async () => { + const { + actions: { clickNameAt, clickDeletDataStreamButton, clickConfirmDelete }, + } = testBed; + + await clickNameAt(0); + + clickDeletDataStreamButton(); + + httpRequestsMockHelpers.setDeleteDataStreamResponse({ + results: { + dataStreamsDeleted: ['dataStream1'], + errors: [], + }, + }); + + await clickConfirmDelete(); + + const { method, url, requestBody } = server.requests[server.requests.length - 1]; + + expect(method).toBe('POST'); + expect(url).toBe(`${API_BASE_PATH}/delete_data_streams`); + expect(JSON.parse(JSON.parse(requestBody).body)).toEqual({ + dataStreams: ['dataStream1'], + }); + }); + }); }); }); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts index f00348aacbf08..11ea29fd9b78c 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.helpers.ts @@ -5,6 +5,7 @@ */ import { act } from 'react-dom/test-utils'; +import { ReactWrapper } from 'enzyme'; import { registerTestBed, @@ -34,6 +35,8 @@ export interface IndicesTestBed extends TestBed { clickIncludeHiddenIndicesToggle: () => void; clickDataStreamAt: (index: number) => void; }; + findDataStreamDetailPanel: () => ReactWrapper; + findDataStreamDetailPanelTitle: () => string; } export const setup = async (): Promise => { @@ -77,6 +80,16 @@ export const setup = async (): Promise => { component.update(); }; + const findDataStreamDetailPanel = () => { + const { find } = testBed; + return find('dataStreamDetailPanel'); + }; + + const findDataStreamDetailPanelTitle = () => { + const { find } = testBed; + return find('dataStreamDetailPanelTitle').text(); + }; + return { ...testBed, actions: { @@ -85,5 +98,7 @@ export const setup = async (): Promise => { clickIncludeHiddenIndicesToggle, clickDataStreamAt, }, + findDataStreamDetailPanel, + findDataStreamDetailPanelTitle, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index c2d955bb4dfce..3d6d94d165855 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -70,10 +70,10 @@ describe('', () => { }, ]); - httpRequestsMockHelpers.setLoadDataStreamsResponse([ - createDataStreamPayload('dataStream1'), - createDataStreamPayload('dataStream2'), - ]); + // The detail panel should still appear even if there are no data streams. + httpRequestsMockHelpers.setLoadDataStreamsResponse([]); + + httpRequestsMockHelpers.setLoadDataStreamResponse(createDataStreamPayload('dataStream1')); testBed = await setup(); @@ -86,13 +86,16 @@ describe('', () => { }); test('navigates to the data stream in the Data Streams tab', async () => { - const { table, actions } = testBed; + const { + findDataStreamDetailPanel, + findDataStreamDetailPanelTitle, + actions: { clickDataStreamAt }, + } = testBed; - await actions.clickDataStreamAt(0); + await clickDataStreamAt(0); - expect(table.getMetaData('dataStreamTable').tableCellsValues).toEqual([ - ['dataStream1', '1', '@timestamp', '1'], - ]); + expect(findDataStreamDetailPanel().length).toBe(1); + expect(findDataStreamDetailPanelTitle()).toBe('dataStream1'); }); }); diff --git a/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts index 9d267210a6b31..51528ed9856ce 100644 --- a/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/data_stream_serialization.ts @@ -6,8 +6,10 @@ import { DataStream, DataStreamFromEs } from '../types'; -export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[]): DataStream[] { - return dataStreamsFromEs.map(({ name, timestamp_field, indices, generation }) => ({ +export function deserializeDataStream(dataStreamFromEs: DataStreamFromEs): DataStream { + const { name, timestamp_field, indices, generation } = dataStreamFromEs; + + return { name, timeStampField: timestamp_field, indices: indices.map( @@ -17,5 +19,9 @@ export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[]) }) ), generation, - })); + }; +} + +export function deserializeDataStreamList(dataStreamsFromEs: DataStreamFromEs[]): DataStream[] { + return dataStreamsFromEs.map((dataStream) => deserializeDataStream(dataStream)); } diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts index fce4d8ccc2502..4e76a40ced524 100644 --- a/x-pack/plugins/index_management/common/lib/index.ts +++ b/x-pack/plugins/index_management/common/lib/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { deserializeDataStreamList } from './data_stream_serialization'; +export { deserializeDataStream, deserializeDataStreamList } from './data_stream_serialization'; export { deserializeLegacyTemplateList, diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json index 2e0fa04337b40..40ecb26e8f0c9 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -10,7 +10,8 @@ ], "optionalPlugins": [ "security", - "usageCollection" + "usageCollection", + "ingestManager" ], "configPath": ["xpack", "index_management"] } diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 84938de416941..c821907120373 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -6,9 +6,10 @@ import React, { createContext, useContext } from 'react'; import { ScopedHistory } from 'kibana/public'; -import { CoreStart } from '../../../../../src/core/public'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; -import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public'; +import { CoreStart } from '../../../../../src/core/public'; +import { IngestManagerSetup } from '../../../ingest_manager/public'; import { IndexMgmtMetricsType } from '../types'; import { UiMetricService, NotificationService, HttpService } from './services'; import { ExtensionsService } from '../services'; @@ -22,6 +23,7 @@ export interface AppDependencies { }; plugins: { usageCollection: UsageCollectionSetup; + ingestManager?: IngestManagerSetup; }; services: { uiMetricService: UiMetricService; diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index e8b6f200fb349..258f32865720a 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -8,6 +8,7 @@ import { CoreSetup } from 'src/core/public'; import { ManagementAppMountParams } from 'src/plugins/management/public/'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { IngestManagerSetup } from '../../../ingest_manager/public'; import { ExtensionsService } from '../services'; import { IndexMgmtMetricsType } from '../types'; import { AppDependencies } from './app_context'; @@ -28,7 +29,8 @@ export async function mountManagementSection( coreSetup: CoreSetup, usageCollection: UsageCollectionSetup, services: InternalServices, - params: ManagementAppMountParams + params: ManagementAppMountParams, + ingestManager?: IngestManagerSetup ) { const { element, setBreadcrumbs, history } = params; const [core] = await coreSetup.getStartServices(); @@ -44,6 +46,7 @@ export async function mountManagementSection( }, plugins: { usageCollection, + ingestManager, }, services, history, diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index a6c8b83a05f98..577f04a4a7efd 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { + EuiButton, EuiFlyout, EuiFlyoutHeader, EuiTitle, @@ -15,14 +16,18 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, } from '@elastic/eui'; import { SectionLoading, SectionError, Error } from '../../../../components'; import { useLoadDataStream } from '../../../../services/api'; +import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; interface Props { dataStreamName: string; - onClose: () => void; + onClose: (shouldReload?: boolean) => void; } /** @@ -36,6 +41,8 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ }) => { const { error, data: dataStream, isLoading } = useLoadDataStream(dataStreamName); + const [isDeleting, setIsDeleting] = useState(false); + let content; if (isLoading) { @@ -61,44 +68,97 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ /> ); } else if (dataStream) { - content = {JSON.stringify(dataStream)}; + const { timeStampField, generation } = dataStream; + + content = ( + + + + + + {timeStampField.name} + + + + + + {generation} + + ); } return ( - - - -

- {dataStreamName} -

-
-
- - {content} - - - - - - - - - - -
+ <> + {isDeleting ? ( + { + if (data && data.hasDeletedDataStreams) { + onClose(true); + } else { + setIsDeleting(false); + } + }} + dataStreams={[dataStreamName]} + /> + ) : null} + + + + +

+ {dataStreamName} +

+
+
+ + {content} + + + + + onClose()} + data-test-subj="closeDetailsButton" + > + + + + + {!isLoading && !error ? ( + + setIsDeleting(true)} + data-test-subj="deleteDataStreamButton" + > + + + + ) : null} + + +
+ ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index 951c4a0d7f3c3..bad008b665cfb 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -12,9 +12,13 @@ import { EuiTitle, EuiText, EuiSpacer, EuiEmptyPrompt, EuiLink } from '@elastic/ import { ScopedHistory } from 'kibana/public'; import { reactRouterNavigate } from '../../../../shared_imports'; +import { useAppContext } from '../../../app_context'; import { SectionError, SectionLoading, Error } from '../../../components'; import { useLoadDataStreams } from '../../../services/api'; +import { decodePathFromReactRouter } from '../../../services/routing'; +import { Section } from '../../home'; import { DataStreamTable } from './data_stream_table'; +import { DataStreamDetailPanel } from './data_stream_detail_panel'; interface MatchParams { dataStreamName?: string; @@ -26,6 +30,11 @@ export const DataStreamList: React.FunctionComponent { + const { + core: { getUrlForApp }, + plugins: { ingestManager }, + } = useAppContext(); + const { error, isLoading, data: dataStreams, sendRequest: reload } = useLoadDataStreams(); let content; @@ -67,22 +76,52 @@ export const DataStreamList: React.FunctionComponent - {i18n.translate('xpack.idxMgmt.dataStreamList.emptyPrompt.getStartedLink', { - defaultMessage: 'composable index template', - })} - - ), - }} + defaultMessage="Data streams represent collections of time series indices." /> + {' ' /* We need this space to separate these two sentences. */} + {ingestManager ? ( + + {i18n.translate( + 'xpack.idxMgmt.dataStreamList.emptyPrompt.noDataStreamsCtaIngestManagerLink', + { + defaultMessage: 'Ingest Manager', + } + )} + + ), + }} + /> + ) : ( + + {i18n.translate( + 'xpack.idxMgmt.dataStreamList.emptyPrompt.noDataStreamsCtaIndexTemplateLink', + { + defaultMessage: 'composable index template', + } + )} + + ), + }} + /> + )}

} data-test-subj="emptyPrompt" @@ -104,24 +143,38 @@ export const DataStreamList: React.FunctionComponent - - {/* TODO: Implement this once we have something to put in here, e.g. storage size, docs count */} - {/* dataStreamName && ( - { - history.push('/data_streams'); - }} - /> - )*/} ); } - return
{content}
; + return ( +
+ {content} + + {/* + If the user has been deep-linked, they'll expect to see the detail panel because it reflects + the URL state, even if there are no data streams or if there was an error loading them. + */} + {dataStreamName && ( + { + history.push(`/${Section.DataStreams}`); + + // If the data stream was deleted, we need to refresh the list. + if (shouldReload) { + reload(); + } + }} + /> + )} +
+ ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx index 54035e2193624..d01d8fa03a3fa 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_table/data_stream_table.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton, EuiLink } from '@elastic/eui'; @@ -13,6 +13,8 @@ import { ScopedHistory } from 'kibana/public'; import { DataStream } from '../../../../../../common/types'; import { reactRouterNavigate } from '../../../../../shared_imports'; import { encodePathForReactRouter } from '../../../../services/routing'; +import { Section } from '../../../home'; +import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; interface Props { dataStreams?: DataStream[]; @@ -27,6 +29,9 @@ export const DataStreamTable: React.FunctionComponent = ({ history, filters, }) => { + const [selection, setSelection] = useState([]); + const [dataStreamsToDelete, setDataStreamsToDelete] = useState([]); + const columns: Array> = [ { field: 'name', @@ -35,7 +40,19 @@ export const DataStreamTable: React.FunctionComponent = ({ }), truncateText: true, sortable: true, - // TODO: Render as a link to open the detail panel + render: (name: DataStream['name'], item: DataStream) => { + return ( + /* eslint-disable-next-line @elastic/eui/href-or-on-click */ + + {name} + + ); + }, }, { field: 'indices', @@ -59,20 +76,27 @@ export const DataStreamTable: React.FunctionComponent = ({ ), }, { - field: 'timeStampField.name', - name: i18n.translate('xpack.idxMgmt.dataStreamList.table.timeStampFieldColumnTitle', { - defaultMessage: 'Timestamp field', + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionColumnTitle', { + defaultMessage: 'Actions', }), - truncateText: true, - sortable: true, - }, - { - field: 'generation', - name: i18n.translate('xpack.idxMgmt.dataStreamList.table.generationFieldColumnTitle', { - defaultMessage: 'Generation', - }), - truncateText: true, - sortable: true, + actions: [ + { + name: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteText', { + defaultMessage: 'Delete', + }), + description: i18n.translate('xpack.idxMgmt.dataStreamList.table.actionDeleteDecription', { + defaultMessage: 'Delete this data stream', + }), + icon: 'trash', + color: 'danger', + type: 'icon', + onClick: ({ name }: DataStream) => { + setDataStreamsToDelete([name]); + }, + isPrimary: true, + 'data-test-subj': 'deleteDataStream', + }, + ], }, ]; @@ -88,12 +112,29 @@ export const DataStreamTable: React.FunctionComponent = ({ }, } as const; + const selectionConfig = { + onSelectionChange: setSelection, + }; + const searchConfig = { query: filters, box: { incremental: true, }, - toolsLeft: undefined /* TODO: Actions menu */, + toolsLeft: + selection.length > 0 ? ( + setDataStreamsToDelete(selection.map(({ name }: DataStream) => name))} + color="danger" + > + + + ) : undefined, toolsRight: [ = ({ return ( <> + {dataStreamsToDelete && dataStreamsToDelete.length > 0 ? ( + { + if (data && data.hasDeletedDataStreams) { + reload(); + } else { + setDataStreamsToDelete([]); + } + }} + dataStreams={dataStreamsToDelete} + /> + ) : null} = ({ search={searchConfig} sorting={sorting} isSelectable={true} + selection={selectionConfig} pagination={pagination} rowProps={() => ({ 'data-test-subj': 'row', diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx new file mode 100644 index 0000000000000..fc8e41aa634b4 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx @@ -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 React, { Fragment } from 'react'; +import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { deleteDataStreams } from '../../../../services/api'; +import { notificationService } from '../../../../services/notification'; + +interface Props { + dataStreams: string[]; + onClose: (data?: { hasDeletedDataStreams: boolean }) => void; +} + +export const DeleteDataStreamConfirmationModal: React.FunctionComponent = ({ + dataStreams, + onClose, +}: { + dataStreams: string[]; + onClose: (data?: { hasDeletedDataStreams: boolean }) => void; +}) => { + const dataStreamsCount = dataStreams.length; + + const handleDeleteDataStreams = () => { + deleteDataStreams(dataStreams).then(({ data: { dataStreamsDeleted, errors }, error }) => { + const hasDeletedDataStreams = dataStreamsDeleted && dataStreamsDeleted.length; + + if (hasDeletedDataStreams) { + const successMessage = + dataStreamsDeleted.length === 1 + ? i18n.translate( + 'xpack.idxMgmt.deleteDataStreamsConfirmationModal.successDeleteSingleNotificationMessageText', + { + defaultMessage: "Deleted data stream '{dataStreamName}'", + values: { dataStreamName: dataStreams[0] }, + } + ) + : i18n.translate( + 'xpack.idxMgmt.deleteDataStreamsConfirmationModal.successDeleteMultipleNotificationMessageText', + { + defaultMessage: + 'Deleted {numSuccesses, plural, one {# data stream} other {# data streams}}', + values: { numSuccesses: dataStreamsDeleted.length }, + } + ); + + onClose({ hasDeletedDataStreams }); + notificationService.showSuccessToast(successMessage); + } + + if (error || (errors && errors.length)) { + const hasMultipleErrors = + (errors && errors.length > 1) || (error && dataStreams.length > 1); + + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.idxMgmt.deleteDataStreamsConfirmationModal.multipleErrorsNotificationMessageText', + { + defaultMessage: 'Error deleting {count} data streams', + values: { + count: (errors && errors.length) || dataStreams.length, + }, + } + ) + : i18n.translate( + 'xpack.idxMgmt.deleteDataStreamsConfirmationModal.errorNotificationMessageText', + { + defaultMessage: "Error deleting data stream '{name}'", + values: { name: (errors && errors[0].name) || dataStreams[0] }, + } + ); + + notificationService.showDangerToast(errorMessage); + } + }); + }; + + return ( + + + } + onCancel={() => onClose()} + onConfirm={handleDeleteDataStreams} + cancelButtonText={ + + } + confirmButtonText={ + + } + > + + + } + color="danger" + iconType="alert" + > +

+ +

+
+ + + +

+ +

+ +
    + {dataStreams.map((name) => ( +
  • {name}
  • + ))} +
+
+
+
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/index.ts b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/index.ts new file mode 100644 index 0000000000000..eaa4a8fc2de02 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/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 { DeleteDataStreamConfirmationModal } from './delete_data_stream_confirmation_modal'; diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts index 6743e7a7c4403..e831068f03aef 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -54,14 +54,21 @@ export function useLoadDataStreams() { }); } -// TODO: Implement this API endpoint once we have content to surface in the detail panel. export function useLoadDataStream(name: string) { - return useRequest({ - path: `${API_BASE_PATH}/data_stream/${encodeURIComponent(name)}`, + return useRequest({ + path: `${API_BASE_PATH}/data_streams/${encodeURIComponent(name)}`, method: 'get', }); } +export async function deleteDataStreams(dataStreams: string[]) { + return sendRequest({ + path: `${API_BASE_PATH}/delete_data_streams`, + method: 'post', + body: { dataStreams }, + }); +} + export async function loadIndices() { const response = await httpService.httpClient.get(`${API_BASE_PATH}/indices`); return response.data ? response.data : response; diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts index 94d9bccdc63ca..aec25ee3247d6 100644 --- a/x-pack/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -8,6 +8,8 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup } from '../../../../src/core/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { ManagementSetup, ManagementSectionId } from '../../../../src/plugins/management/public'; + +import { IngestManagerSetup } from '../../ingest_manager/public'; import { UIM_APP_NAME, PLUGIN } from '../common/constants'; import { httpService } from './application/services/http'; @@ -25,6 +27,7 @@ export interface IndexManagementPluginSetup { } interface PluginsDependencies { + ingestManager?: IngestManagerSetup; usageCollection: UsageCollectionSetup; management: ManagementSetup; } @@ -42,7 +45,7 @@ export class IndexMgmtUIPlugin { public setup(coreSetup: CoreSetup, plugins: PluginsDependencies): IndexManagementPluginSetup { const { http, notifications } = coreSetup; - const { usageCollection, management } = plugins; + const { ingestManager, usageCollection, management } = plugins; httpService.setup(http); notificationService.setup(notifications); @@ -60,7 +63,7 @@ export class IndexMgmtUIPlugin { uiMetricService: this.uiMetricService, extensionsService: this.extensionsService, }; - return mountManagementSection(coreSetup, usageCollection, services, params); + return mountManagementSection(coreSetup, usageCollection, services, params, ingestManager); }, }); diff --git a/x-pack/plugins/index_management/server/client/elasticsearch.ts b/x-pack/plugins/index_management/server/client/elasticsearch.ts index 6b1bf47512b21..6c0fbe3dd6a65 100644 --- a/x-pack/plugins/index_management/server/client/elasticsearch.ts +++ b/x-pack/plugins/index_management/server/client/elasticsearch.ts @@ -20,6 +20,20 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) method: 'GET', }); + dataManagement.getDataStream = ca({ + urls: [ + { + fmt: '/_data_stream/<%=name%>', + req: { + name: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + // We don't allow the user to create a data stream in the UI or API. We're just adding this here // to enable the API integration tests. dataManagement.createDataStream = ca({ diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts index 56c514e30f242..4aaf2b1bc5ed5 100644 --- a/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/index.ts @@ -6,8 +6,11 @@ import { RouteDependencies } from '../../../types'; -import { registerGetAllRoute } from './register_get_route'; +import { registerGetOneRoute, registerGetAllRoute } from './register_get_route'; +import { registerDeleteRoute } from './register_delete_route'; export function registerDataStreamRoutes(dependencies: RouteDependencies) { + registerGetOneRoute(dependencies); registerGetAllRoute(dependencies); + registerDeleteRoute(dependencies); } diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts new file mode 100644 index 0000000000000..45b185bcd053b --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_delete_route.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../../types'; +import { addBasePath } from '../index'; +import { wrapEsError } from '../../helpers'; + +const bodySchema = schema.object({ + dataStreams: schema.arrayOf(schema.string()), +}); + +export function registerDeleteRoute({ router, license }: RouteDependencies) { + router.post( + { + path: addBasePath('/delete_data_streams'), + validate: { body: bodySchema }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; + const { dataStreams } = req.body as TypeOf; + + const response: { dataStreamsDeleted: string[]; errors: any[] } = { + dataStreamsDeleted: [], + errors: [], + }; + + await Promise.all( + dataStreams.map(async (name: string) => { + try { + await callAsCurrentUser('dataManagement.deleteDataStream', { + name, + }); + + return response.dataStreamsDeleted.push(name); + } catch (e) { + return response.errors.push({ + name, + error: wrapEsError(e), + }); + } + }) + ); + + return res.ok({ body: response }); + }) + ); +} diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts index 9128556130bf4..5f4e625348333 100644 --- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { deserializeDataStreamList } from '../../../../common/lib'; +import { schema, TypeOf } from '@kbn/config-schema'; + +import { deserializeDataStream, deserializeDataStreamList } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; @@ -32,3 +34,40 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou }) ); } + +export function registerGetOneRoute({ router, license, lib: { isEsError } }: RouteDependencies) { + const paramsSchema = schema.object({ + name: schema.string(), + }); + + router.get( + { + path: addBasePath('/data_streams/{name}'), + validate: { params: paramsSchema }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { name } = req.params as TypeOf; + const { callAsCurrentUser } = ctx.dataManagement!.client; + + try { + const dataStream = await callAsCurrentUser('dataManagement.getDataStream', { name }); + + if (dataStream[0]) { + const body = deserializeDataStream(dataStream[0]); + return res.ok({ body }); + } + + return res.notFound(); + } catch (e) { + if (isEsError(e)) { + return res.customError({ + statusCode: e.statusCode, + body: e, + }); + } + // Case: default + return res.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/plugins/ingest_manager/public/index.ts b/x-pack/plugins/ingest_manager/public/index.ts index 9f4893ac6e499..ac56349b30c13 100644 --- a/x-pack/plugins/ingest_manager/public/index.ts +++ b/x-pack/plugins/ingest_manager/public/index.ts @@ -6,7 +6,7 @@ import { PluginInitializerContext } from 'src/core/public'; import { IngestManagerPlugin } from './plugin'; -export { IngestManagerStart } from './plugin'; +export { IngestManagerSetup, IngestManagerStart } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => { return new IngestManagerPlugin(initializerContext); diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index 3eb2fad339b7d..554df1d406849 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -22,7 +22,11 @@ import { registerDatasource } from './applications/ingest_manager/sections/agent export { IngestManagerConfigType } from '../common/types'; -export type IngestManagerSetup = void; +// We need to provide an object instead of void so that dependent plugins know when Ingest Manager +// is disabled. +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface IngestManagerSetup {} + /** * Describes public IngestManager plugin contract returned at the `start` stage. */ @@ -75,6 +79,8 @@ export class IngestManagerPlugin }; }, }); + + return {}; } public async start(core: CoreStart): Promise { From d44144fb8f0e41b87de698d678bef08fe70a60a8 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 25 Jun 2020 10:55:11 +0200 Subject: [PATCH 21/82] [ML] Data Grid Histograms (#68359) (#69607) Adds support for histogram charts to data grid columns. - Adds a toggle button to the data grid's header to enabled/disable column charts. - When enabled, the charts get rendered as part of the data grid header. - Histogram charts will get rendered for fields based on date, number, string and boolean. --- .../ml/common/util/group_color_utils.ts | 16 +- .../ml/common/util/string_utils.test.ts | 10 +- x-pack/plugins/ml/common/util/string_utils.ts | 17 + .../components/data_grid/column_chart.scss | 31 ++ .../components/data_grid/column_chart.tsx | 73 +++ .../components/data_grid/common.ts | 33 +- .../components/data_grid/data_grid.tsx | 78 +++- .../application/components/data_grid/index.ts | 2 + .../application/components/data_grid/types.ts | 12 +- .../components/data_grid/use_column_chart.tsx | 432 ++++++++++++++++++ .../{use_data_grid.ts => use_data_grid.tsx} | 29 +- .../data_frame_analytics/common/fields.ts | 2 +- .../configuration_step_form.tsx | 3 +- .../hooks/use_index_data.ts | 43 +- .../exploration_results_table.tsx | 19 +- .../use_exploration_results.ts | 37 +- .../outlier_exploration.tsx | 59 +-- .../outlier_exploration/use_outlier_data.ts | 33 +- .../public/app/hooks/use_index_data.ts | 40 +- .../public/app/hooks/use_pivot_data.ts | 11 +- .../expanded_row_preview_pane.tsx | 18 +- .../transform/public/shared_imports.ts | 3 + .../apps/transform/creation_index_pattern.ts | 32 +- .../functional/services/transform/wizard.ts | 58 +++ 24 files changed, 996 insertions(+), 95 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/column_chart.scss create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx create mode 100644 x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx rename x-pack/plugins/ml/public/application/components/data_grid/{use_data_grid.ts => use_data_grid.tsx} (79%) diff --git a/x-pack/plugins/ml/common/util/group_color_utils.ts b/x-pack/plugins/ml/common/util/group_color_utils.ts index 92f5c6b2c1347..7105919274185 100644 --- a/x-pack/plugins/ml/common/util/group_color_utils.ts +++ b/x-pack/plugins/ml/common/util/group_color_utils.ts @@ -6,6 +6,8 @@ import euiVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { stringHash } from './string_utils'; + const COLORS = [ euiVars.euiColorVis0, euiVars.euiColorVis1, @@ -33,17 +35,3 @@ export function tabColor(name: string): string { return colorMap[name]; } } - -function stringHash(str: string): number { - let hash = 0; - let chr = 0; - if (str.length === 0) { - return hash; - } - for (let i = 0; i < str.length; i++) { - chr = str.charCodeAt(i); - hash = (hash << 5) - hash + chr; // eslint-disable-line no-bitwise - hash |= 0; // eslint-disable-line no-bitwise - } - return hash < 0 ? hash * -2 : hash; -} diff --git a/x-pack/plugins/ml/common/util/string_utils.test.ts b/x-pack/plugins/ml/common/util/string_utils.test.ts index 026c8e6110c99..8afc7e52c9fa5 100644 --- a/x-pack/plugins/ml/common/util/string_utils.test.ts +++ b/x-pack/plugins/ml/common/util/string_utils.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderTemplate, getMedianStringLength } from './string_utils'; +import { renderTemplate, getMedianStringLength, stringHash } from './string_utils'; const strings: string[] = [ 'foo', @@ -46,4 +46,12 @@ describe('ML - string utils', () => { expect(result).toBe(0); }); }); + + describe('stringHash', () => { + test('should return a unique number based off a string', () => { + const hash1 = stringHash('the-string-1'); + const hash2 = stringHash('the-string-2'); + expect(hash1).not.toBe(hash2); + }); + }); }); diff --git a/x-pack/plugins/ml/common/util/string_utils.ts b/x-pack/plugins/ml/common/util/string_utils.ts index bd4ca02bf93cc..b4591fd2943e6 100644 --- a/x-pack/plugins/ml/common/util/string_utils.ts +++ b/x-pack/plugins/ml/common/util/string_utils.ts @@ -22,3 +22,20 @@ export function getMedianStringLength(strings: string[]) { const sortedStringLengths = strings.map((s) => s.length).sort((a, b) => a - b); return sortedStringLengths[Math.floor(sortedStringLengths.length / 2)] || 0; } + +/** + * Creates a deterministic number based hash out of a string. + */ +export function stringHash(str: string): number { + let hash = 0; + let chr = 0; + if (str.length === 0) { + return hash; + } + for (let i = 0; i < str.length; i++) { + chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr; // eslint-disable-line no-bitwise + hash |= 0; // eslint-disable-line no-bitwise + } + return hash < 0 ? hash * -2 : hash; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.scss b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.scss new file mode 100644 index 0000000000000..37d8871ab3562 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.scss @@ -0,0 +1,31 @@ +.mlDataGridChart__histogram { + width: 100%; + height: $euiSizeXL + $euiSizeXXL; +} + +.mlDataGridChart__legend { + @include euiTextTruncate; + @include euiFontSizeXS; + + color: $euiColorMediumShade; + display: block; + overflow-x: hidden; + margin: $euiSizeXS 0px 0px 0px; + font-style: italic; + font-weight: normal; + text-align: left; +} + +.mlDataGridChart__legend--numeric { + text-align: right; +} + +.mlDataGridChart__legendBoolean { + width: 100%; + td { text-align: center } +} + +/* Override to align column header to bottom of cell when no chart is available */ +.mlDataGrid .euiDataGridHeaderCell__content { + margin-top: auto; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.tsx new file mode 100644 index 0000000000000..00e2d5b14a96b --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.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 classNames from 'classnames'; + +import { BarSeries, Chart, Settings } from '@elastic/charts'; +import { EuiDataGridColumn } from '@elastic/eui'; + +import './column_chart.scss'; + +import { isUnsupportedChartData, useColumnChart, ChartData } from './use_column_chart'; + +interface Props { + chartData: ChartData; + columnType: EuiDataGridColumn; + dataTestSubj: string; +} + +export const ColumnChart: FC = ({ chartData, columnType, dataTestSubj }) => { + const { data, legendText, xScaleType } = useColumnChart(chartData, columnType); + + return ( +
+ {!isUnsupportedChartData(chartData) && data.length > 0 && ( +
+ + + d.datum.color} + data={data} + /> + +
+ )} +
+ {legendText} +
+
{columnType.id}
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 7d0559c215114..1f0fcb63f019d 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -13,6 +13,10 @@ import { EuiDataGridStyle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { CoreSetup } from 'src/core/public'; + import { IndexPattern, IFieldType, @@ -20,6 +24,8 @@ import { KBN_FIELD_TYPES, } from '../../../../../../../src/plugins/data/public'; +import { extractErrorMessage } from '../../../../common/util/errors'; + import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES, @@ -37,7 +43,7 @@ import { mlFieldFormatService } from '../../services/field_format_service'; import { DataGridItem, IndexPagination, RenderCellValue } from './types'; -export const INIT_MAX_COLUMNS = 20; +export const INIT_MAX_COLUMNS = 10; export const euiDataGridStyle: EuiDataGridStyle = { border: 'all', @@ -102,6 +108,8 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results case 'boolean': schema = 'boolean'; break; + case 'text': + schema = NON_AGGREGATABLE; } if ( @@ -122,7 +130,10 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results }); }; -export const getDataGridSchemaFromKibanaFieldType = (field: IFieldType | undefined) => { +export const NON_AGGREGATABLE = 'non-aggregatable'; +export const getDataGridSchemaFromKibanaFieldType = ( + field: IFieldType | undefined +): string | undefined => { // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] // To fall back to the default string schema it needs to be undefined. let schema; @@ -143,6 +154,10 @@ export const getDataGridSchemaFromKibanaFieldType = (field: IFieldType | undefin break; } + if (schema === undefined && field?.aggregatable === false) { + return NON_AGGREGATABLE; + } + return schema; }; @@ -289,3 +304,17 @@ export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['colum return sortFn; }; + +export const showDataGridColumnChartErrorMessageToast = ( + e: any, + toastNotifications: CoreSetup['notifications']['toasts'] +) => { + const error = extractErrorMessage(e); + + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataGrid.columnChart.ErrorMessageToast', { + defaultMessage: 'An error occurred fetching the histogram charts data: {error}', + values: { error: error !== '' ? error : e }, + }) + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index 618075a77d906..9af7a869e0e56 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -10,6 +10,7 @@ import React, { memo, useEffect, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { + EuiButtonEmpty, EuiButtonIcon, EuiCallOut, EuiCodeBlock, @@ -27,6 +28,8 @@ import { INDEX_STATUS } from '../../data_frame_analytics/common'; import { euiDataGridStyle, euiDataGridToolbarSettings } from './common'; import { UseIndexDataReturnType } from './types'; +// TODO Fix row hovering + bar highlighting +// import { hoveredRow$ } from './column_chart'; export const DataGridTitle: FC<{ title: string }> = ({ title }) => ( @@ -54,7 +57,9 @@ type Props = PropsWithHeader | PropsWithoutHeader; export const DataGrid: FC = memo( (props) => { const { - columns, + chartsVisible, + chartsButtonVisible, + columnsWithCharts, dataTestSubj, errorMessage, invalidSortingColumnns, @@ -70,9 +75,18 @@ export const DataGrid: FC = memo( status, tableItems: data, toastNotifications, + toggleChartVisibility, visibleColumns, } = props; + // TODO Fix row hovering + bar highlighting + // const getRowProps = (item: any) => { + // return { + // onMouseOver: () => hoveredRow$.next(item), + // onMouseLeave: () => hoveredRow$.next(null), + // }; + // }; + useEffect(() => { if (invalidSortingColumnns.length > 0) { invalidSortingColumnns.forEach((columnId) => { @@ -162,22 +176,50 @@ export const DataGrid: FC = memo(
)} - +
+ { + c.initialWidth = 165; + return c; + })} + columnVisibility={{ visibleColumns, setVisibleColumns }} + gridStyle={euiDataGridStyle} + rowCount={rowCount} + renderCellValue={renderCellValue} + sorting={{ columns: sortingColumns, onSort }} + toolbarVisibility={{ + ...euiDataGridToolbarSettings, + ...(chartsButtonVisible + ? { + additionalControls: ( + + {i18n.translate('xpack.ml.dataGrid.histogramButtonText', { + defaultMessage: 'Histogram charts', + })} + + ), + } + : {}), + }} + pagination={{ + ...pagination, + pageSizeOptions: [5, 10, 25], + onChangeItemsPerPage, + onChangePage, + }} + /> +
); }, @@ -186,7 +228,7 @@ export const DataGrid: FC = memo( function pickProps(props: Props) { return [ - props.columns, + props.columnsWithCharts, props.dataTestSubj, props.errorMessage, props.invalidSortingColumnns, diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts index 2472878d1b0c1..80bc6b861f742 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -9,8 +9,10 @@ export { getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, multiColumnSortFactory, + showDataGridColumnChartErrorMessageToast, useRenderCellValue, } from './common'; +export { fetchChartsData, ChartData } from './use_column_chart'; export { useDataGrid } from './use_data_grid'; export { DataGrid } from './data_grid'; export { diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts index 5fa038edf7815..756f74c8f9302 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts @@ -13,6 +13,8 @@ import { Dictionary } from '../../../../common/types/common'; import { INDEX_STATUS } from '../../data_frame_analytics/common/analytics'; +import { ChartData } from './use_column_chart'; + export type ColumnId = string; export type DataGridItem = Record; @@ -54,6 +56,9 @@ export interface SearchResponse7 extends SearchResponse { export interface UseIndexDataReturnType extends Pick< UseDataGridReturnType, + | 'chartsVisible' + | 'chartsButtonVisible' + | 'columnsWithCharts' | 'errorMessage' | 'invalidSortingColumnns' | 'noDataMessage' @@ -67,13 +72,16 @@ export interface UseIndexDataReturnType | 'sortingColumns' | 'status' | 'tableItems' + | 'toggleChartVisibility' | 'visibleColumns' > { - columns: EuiDataGridColumn[]; renderCellValue: RenderCellValue; } export interface UseDataGridReturnType { + chartsVisible: boolean; + chartsButtonVisible: boolean; + columnsWithCharts: EuiDataGridColumn[]; errorMessage: string; invalidSortingColumnns: ColumnId[]; noDataMessage: string; @@ -83,6 +91,7 @@ export interface UseDataGridReturnType { pagination: IndexPagination; resetPagination: () => void; rowCount: number; + setColumnCharts: Dispatch>; setErrorMessage: Dispatch>; setNoDataMessage: Dispatch>; setPagination: Dispatch>; @@ -94,5 +103,6 @@ export interface UseDataGridReturnType { sortingColumns: EuiDataGridSorting['columns']; status: INDEX_STATUS; tableItems: DataGridItem[]; + toggleChartVisibility: () => void; visibleColumns: ColumnId[]; } diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx new file mode 100644 index 0000000000000..6b207a999eb52 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_column_chart.tsx @@ -0,0 +1,432 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { BehaviorSubject } from 'rxjs'; +import React from 'react'; + +import { useObservable } from 'react-use'; + +import { euiPaletteColorBlind, EuiDataGridColumn } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; + +import { stringHash } from '../../../../common/util/string_utils'; + +import { NON_AGGREGATABLE } from './common'; + +export const hoveredRow$ = new BehaviorSubject(null); + +const BAR_COLOR = euiPaletteColorBlind()[0]; +const BAR_COLOR_BLUR = euiPaletteColorBlind({ rotations: 2 })[10]; +const MAX_CHART_COLUMNS = 20; + +type XScaleType = 'ordinal' | 'time' | 'linear' | undefined; +const getXScaleType = (kbnFieldType: KBN_FIELD_TYPES | undefined): XScaleType => { + switch (kbnFieldType) { + case KBN_FIELD_TYPES.BOOLEAN: + case KBN_FIELD_TYPES.IP: + case KBN_FIELD_TYPES.STRING: + return 'ordinal'; + case KBN_FIELD_TYPES.DATE: + return 'time'; + case KBN_FIELD_TYPES.NUMBER: + return 'linear'; + } +}; + +const getFieldType = (schema: EuiDataGridColumn['schema']): KBN_FIELD_TYPES | undefined => { + if (schema === NON_AGGREGATABLE) { + return undefined; + } + + let fieldType: KBN_FIELD_TYPES; + + switch (schema) { + case 'datetime': + fieldType = KBN_FIELD_TYPES.DATE; + break; + case 'numeric': + fieldType = KBN_FIELD_TYPES.NUMBER; + break; + case 'boolean': + fieldType = KBN_FIELD_TYPES.BOOLEAN; + break; + case 'json': + fieldType = KBN_FIELD_TYPES.OBJECT; + break; + default: + fieldType = KBN_FIELD_TYPES.STRING; + } + + return fieldType; +}; + +interface NumericColumnStats { + interval: number; + min: number; + max: number; +} +type NumericColumnStatsMap = Record; +const getAggIntervals = async ( + indexPatternTitle: string, + esSearch: (payload: any) => Promise, + query: any, + columnTypes: EuiDataGridColumn[] +): Promise => { + const numericColumns = columnTypes.filter((cT) => { + const fieldType = getFieldType(cT.schema); + return fieldType === KBN_FIELD_TYPES.NUMBER || fieldType === KBN_FIELD_TYPES.DATE; + }); + + if (numericColumns.length === 0) { + return {}; + } + + const minMaxAggs = numericColumns.reduce((aggs, c) => { + const id = stringHash(c.id); + aggs[id] = { + stats: { + field: c.id, + }, + }; + return aggs; + }, {} as Record); + + const respStats = await esSearch({ + index: indexPatternTitle, + size: 0, + body: { + query, + aggs: minMaxAggs, + size: 0, + }, + }); + + return Object.keys(respStats.aggregations).reduce((p, aggName) => { + const stats = [respStats.aggregations[aggName].min, respStats.aggregations[aggName].max]; + if (!stats.includes(null)) { + const delta = respStats.aggregations[aggName].max - respStats.aggregations[aggName].min; + + let aggInterval = 1; + + if (delta > MAX_CHART_COLUMNS) { + aggInterval = Math.round(delta / MAX_CHART_COLUMNS); + } + + if (delta <= 1) { + aggInterval = delta / MAX_CHART_COLUMNS; + } + + p[aggName] = { interval: aggInterval, min: stats[0], max: stats[1] }; + } + + return p; + }, {} as NumericColumnStatsMap); +}; + +interface AggHistogram { + histogram: { + field: string; + interval: number; + }; +} + +interface AggCardinality { + cardinality: { + field: string; + }; +} + +interface AggTerms { + terms: { + field: string; + size: number; + }; +} + +type ChartRequestAgg = AggHistogram | AggCardinality | AggTerms; + +export const fetchChartsData = async ( + indexPatternTitle: string, + esSearch: (payload: any) => Promise, + query: any, + columnTypes: EuiDataGridColumn[] +): Promise => { + const aggIntervals = await getAggIntervals(indexPatternTitle, esSearch, query, columnTypes); + + const chartDataAggs = columnTypes.reduce((aggs, c) => { + const fieldType = getFieldType(c.schema); + const id = stringHash(c.id); + if (fieldType === KBN_FIELD_TYPES.NUMBER || fieldType === KBN_FIELD_TYPES.DATE) { + if (aggIntervals[id] !== undefined) { + aggs[`${id}_histogram`] = { + histogram: { + field: c.id, + interval: aggIntervals[id].interval !== 0 ? aggIntervals[id].interval : 1, + }, + }; + } + } else if (fieldType === KBN_FIELD_TYPES.STRING || fieldType === KBN_FIELD_TYPES.BOOLEAN) { + if (fieldType === KBN_FIELD_TYPES.STRING) { + aggs[`${id}_cardinality`] = { + cardinality: { + field: c.id, + }, + }; + } + aggs[`${id}_terms`] = { + terms: { + field: c.id, + size: MAX_CHART_COLUMNS, + }, + }; + } + return aggs; + }, {} as Record); + + if (Object.keys(chartDataAggs).length === 0) { + return []; + } + + const respChartsData = await esSearch({ + index: indexPatternTitle, + size: 0, + body: { + query, + aggs: chartDataAggs, + size: 0, + }, + }); + + const chartsData: ChartData[] = columnTypes.map( + (c): ChartData => { + const fieldType = getFieldType(c.schema); + const id = stringHash(c.id); + + if (fieldType === KBN_FIELD_TYPES.NUMBER || fieldType === KBN_FIELD_TYPES.DATE) { + if (aggIntervals[id] === undefined) { + return { + type: 'numeric', + data: [], + interval: 0, + stats: [0, 0], + id: c.id, + }; + } + + return { + data: respChartsData.aggregations[`${id}_histogram`].buckets, + interval: aggIntervals[id].interval, + stats: [aggIntervals[id].min, aggIntervals[id].max], + type: 'numeric', + id: c.id, + }; + } else if (fieldType === KBN_FIELD_TYPES.STRING || fieldType === KBN_FIELD_TYPES.BOOLEAN) { + return { + type: fieldType === KBN_FIELD_TYPES.STRING ? 'ordinal' : 'boolean', + cardinality: + fieldType === KBN_FIELD_TYPES.STRING + ? respChartsData.aggregations[`${id}_cardinality`].value + : 2, + data: respChartsData.aggregations[`${id}_terms`].buckets, + id: c.id, + }; + } + + return { + type: 'unsupported', + id: c.id, + }; + } + ); + + return chartsData; +}; + +interface NumericDataItem { + key: number; + key_as_string?: string; + doc_count: number; +} + +interface NumericChartData { + data: NumericDataItem[]; + id: string; + interval: number; + stats: [number, number]; + type: 'numeric'; +} + +export const isNumericChartData = (arg: any): arg is NumericChartData => { + return ( + arg.hasOwnProperty('data') && + arg.hasOwnProperty('id') && + arg.hasOwnProperty('interval') && + arg.hasOwnProperty('stats') && + arg.hasOwnProperty('type') + ); +}; + +interface OrdinalDataItem { + key: string; + key_as_string?: string; + doc_count: number; +} + +interface OrdinalChartData { + type: 'ordinal' | 'boolean'; + cardinality: number; + data: OrdinalDataItem[]; + id: string; +} + +export const isOrdinalChartData = (arg: any): arg is OrdinalChartData => { + return ( + arg.hasOwnProperty('data') && + arg.hasOwnProperty('cardinality') && + arg.hasOwnProperty('id') && + arg.hasOwnProperty('type') + ); +}; + +interface UnsupportedChartData { + id: string; + type: 'unsupported'; +} + +export const isUnsupportedChartData = (arg: any): arg is UnsupportedChartData => { + return arg.hasOwnProperty('type') && arg.type === 'unsupported'; +}; + +type ChartDataItem = NumericDataItem | OrdinalDataItem; +export type ChartData = NumericChartData | OrdinalChartData | UnsupportedChartData; + +type LegendText = string | JSX.Element; +const getLegendText = (chartData: ChartData): LegendText => { + if (chartData.type === 'unsupported') { + return i18n.translate('xpack.ml.dataGridChart.histogramNotAvailable', { + defaultMessage: 'Chart not supported.', + }); + } + + if (chartData.data.length === 0) { + return i18n.translate('xpack.ml.dataGridChart.notEnoughData', { + defaultMessage: `0 documents contain field.`, + }); + } + + if (chartData.type === 'boolean') { + return ( + + + + {chartData.data[0] !== undefined && } + {chartData.data[1] !== undefined && } + + +
{chartData.data[0].key_as_string}{chartData.data[1].key_as_string}
+ ); + } + + if (isOrdinalChartData(chartData) && chartData.cardinality <= MAX_CHART_COLUMNS) { + return i18n.translate('xpack.ml.dataGridChart.singleCategoryLegend', { + defaultMessage: `{cardinality, plural, one {# category} other {# categories}}`, + values: { cardinality: chartData.cardinality }, + }); + } + + if (isOrdinalChartData(chartData) && chartData.cardinality > MAX_CHART_COLUMNS) { + return i18n.translate('xpack.ml.dataGridChart.topCategoriesLegend', { + defaultMessage: `top {MAX_CHART_COLUMNS} of {cardinality} categories`, + values: { cardinality: chartData.cardinality, MAX_CHART_COLUMNS }, + }); + } + + if (isNumericChartData(chartData)) { + const fromValue = Math.round(chartData.stats[0] * 100) / 100; + const toValue = Math.round(chartData.stats[1] * 100) / 100; + + return fromValue !== toValue ? `${fromValue} - ${toValue}` : '' + fromValue; + } + + return ''; +}; + +interface ColumnChart { + data: ChartDataItem[]; + legendText: LegendText; + xScaleType: XScaleType; +} + +export const useColumnChart = ( + chartData: ChartData, + columnType: EuiDataGridColumn +): ColumnChart => { + const fieldType = getFieldType(columnType.schema); + + const hoveredRow = useObservable(hoveredRow$); + + const xScaleType = getXScaleType(fieldType); + + const getColor = (d: ChartDataItem) => { + if (hoveredRow === undefined || hoveredRow === null) { + return BAR_COLOR; + } + + if ( + isOrdinalChartData(chartData) && + xScaleType === 'ordinal' && + hoveredRow._source[columnType.id] === d.key + ) { + return BAR_COLOR; + } + + if ( + isNumericChartData(chartData) && + xScaleType === 'linear' && + hoveredRow._source[columnType.id] >= +d.key && + hoveredRow._source[columnType.id] < +d.key + chartData.interval + ) { + return BAR_COLOR; + } + + if ( + isNumericChartData(chartData) && + xScaleType === 'time' && + moment(hoveredRow._source[columnType.id]).unix() * 1000 >= +d.key && + moment(hoveredRow._source[columnType.id]).unix() * 1000 < +d.key + chartData.interval + ) { + return BAR_COLOR; + } + + return BAR_COLOR_BLUR; + }; + + let data: ChartDataItem[] = []; + + // The if/else if/else is a work-around because `.map()` doesn't work with union types. + // See TS Caveats for details: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#caveats + if (isOrdinalChartData(chartData)) { + data = chartData.data.map((d: OrdinalDataItem) => ({ + ...d, + color: getColor(d), + })); + } else if (isNumericChartData(chartData)) { + data = chartData.data.map((d: NumericDataItem) => ({ + ...d, + color: getColor(d), + })); + } + + return { + data, + legendText: getLegendText(chartData), + xScaleType, + }; +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx similarity index 79% rename from x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts rename to x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx index 7843bf2ea801b..47c84ce6cd664 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; import { INDEX_STATUS } from '../../data_frame_analytics/common'; +import { ColumnChart } from './column_chart'; import { INIT_MAX_COLUMNS } from './common'; import { ColumnId, @@ -20,6 +21,7 @@ import { OnSort, UseDataGridReturnType, } from './types'; +import { ChartData } from './use_column_chart'; export const useDataGrid = ( columns: EuiDataGridColumn[], @@ -33,9 +35,15 @@ export const useDataGrid = ( const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(INDEX_STATUS.UNUSED); const [rowCount, setRowCount] = useState(0); + const [columnCharts, setColumnCharts] = useState([]); const [tableItems, setTableItems] = useState([]); const [pagination, setPagination] = useState(defaultPagination); const [sortingColumns, setSortingColumns] = useState([]); + const [chartsVisible, setChartsVisible] = useState(false); + + const toggleChartVisibility = () => { + setChartsVisible(!chartsVisible); + }; const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback((pageSize) => { setPagination((p) => { @@ -87,6 +95,23 @@ export const useDataGrid = ( ); return { + chartsVisible, + chartsButtonVisible: true, + columnsWithCharts: columns.map((c, index) => { + const chartData = columnCharts.find((cd) => cd.id === c.id); + + return { + ...c, + display: + chartData !== undefined && chartsVisible === true ? ( + + ) : undefined, + }; + }), errorMessage, invalidSortingColumnns, noDataMessage, @@ -96,6 +121,7 @@ export const useDataGrid = ( pagination, resetPagination, rowCount, + setColumnCharts, setErrorMessage, setNoDataMessage, setPagination, @@ -107,6 +133,7 @@ export const useDataGrid = ( sortingColumns, status, tableItems, + toggleChartVisibility, visibleColumns, }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index 0a64886c80a63..1b28875a624f8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -30,7 +30,7 @@ export interface EsDoc extends Record { _source: EsDocSource; } -export const MAX_COLUMNS = 20; +export const MAX_COLUMNS = 10; export const DEFAULT_REGRESSION_COLUMNS = 8; export const BASIC_NUMERICAL_TYPES = new Set([ diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index e63756686a4ba..2c3079e24cf1d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -87,7 +87,8 @@ export const ConfigurationStepForm: FC = ({ const indexData = useIndexData( currentIndexPattern, - savedSearchQuery !== undefined ? savedSearchQuery : jobConfigQuery + savedSearchQuery !== undefined ? savedSearchQuery : jobConfigQuery, + toastNotifications ); const indexPreviewProps = { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts index e8f25584201e3..ee0e5c1955ead 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -6,10 +6,16 @@ import { useEffect } from 'react'; +import { EuiDataGridColumn } from '@elastic/eui'; + +import { CoreSetup } from 'src/core/public'; + import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { + fetchChartsData, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, + showDataGridColumnChartErrorMessageToast, useDataGrid, useRenderCellValue, EsSorting, @@ -22,15 +28,19 @@ import { ml } from '../../../../services/ml_api_service'; type IndexSearchResponse = SearchResponse7; -export const useIndexData = (indexPattern: IndexPattern, query: any): UseIndexDataReturnType => { +export const useIndexData = ( + indexPattern: IndexPattern, + query: any, + toastNotifications: CoreSetup['notifications']['toasts'] +): UseIndexDataReturnType => { const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); // EuiDataGrid State - const columns = [ + const columns: EuiDataGridColumn[] = [ ...indexPatternFields.map((id) => { const field = indexPattern.fields.getByName(id); const schema = getDataGridSchemaFromKibanaFieldType(field); - return { id, schema }; + return { id, schema, isExpandable: schema !== 'boolean' }; }), ]; @@ -93,11 +103,36 @@ export const useIndexData = (indexPattern: IndexPattern, query: any): UseIndexDa // eslint-disable-next-line react-hooks/exhaustive-deps }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + const fetchColumnChartsData = async function () { + try { + const columnChartsData = await fetchChartsData( + indexPattern.title, + ml.esSearch, + query, + columns.filter((cT) => dataGrid.visibleColumns.includes(cT.id)) + ); + dataGrid.setColumnCharts(columnChartsData); + } catch (e) { + showDataGridColumnChartErrorMessageToast(e, toastNotifications); + } + }; + + useEffect(() => { + if (dataGrid.chartsVisible) { + fetchColumnChartsData(); + } + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + dataGrid.chartsVisible, + indexPattern.title, + JSON.stringify([query, dataGrid.visibleColumns]), + ]); + const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); return { ...dataGrid, - columns, renderCellValue, }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index 105eb9f73804d..941fbefd78084 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -67,9 +67,20 @@ export const ExplorationResultsTable: FC = React.memo( setEvaluateSearchQuery(searchQuery); }, [JSON.stringify(searchQuery)]); - const classificationData = useExplorationResults(indexPattern, jobConfig, searchQuery); - const docFieldsCount = classificationData.columns.length; - const { columns, errorMessage, status, tableItems, visibleColumns } = classificationData; + const classificationData = useExplorationResults( + indexPattern, + jobConfig, + searchQuery, + getToastNotifications() + ); + const docFieldsCount = classificationData.columnsWithCharts.length; + const { + columnsWithCharts, + errorMessage, + status, + tableItems, + visibleColumns, + } = classificationData; if (jobConfig === undefined || classificationData === undefined) { return null; @@ -140,7 +151,7 @@ export const ExplorationResultsTable: FC = React.memo( - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( + {(columnsWithCharts.length > 0 || searchQuery !== defaultSearchQuery) && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts index b8b5a16c84e85..796670f6a864d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -8,15 +8,20 @@ import { useEffect } from 'react'; import { EuiDataGridColumn } from '@elastic/eui'; +import { CoreSetup } from 'src/core/public'; + import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { + fetchChartsData, getDataGridSchemasFromFieldTypes, + showDataGridColumnChartErrorMessageToast, useDataGrid, useRenderCellValue, UseIndexDataReturnType, } from '../../../../../components/data_grid'; import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { ml } from '../../../../../services/ml_api_service'; import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; import { @@ -29,7 +34,8 @@ import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fi export const useExplorationResults = ( indexPattern: IndexPattern | undefined, jobConfig: DataFrameAnalyticsConfig | undefined, - searchQuery: SavedSearchQuery + searchQuery: SavedSearchQuery, + toastNotifications: CoreSetup['notifications']['toasts'] ): UseIndexDataReturnType => { const needsDestIndexFields = indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; @@ -66,6 +72,34 @@ export const useExplorationResults = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); + const fetchColumnChartsData = async function () { + try { + if (jobConfig !== undefined) { + const columnChartsData = await fetchChartsData( + jobConfig.dest.index, + ml.esSearch, + searchQuery, + columns.filter((cT) => dataGrid.visibleColumns.includes(cT.id)) + ); + dataGrid.setColumnCharts(columnChartsData); + } + } catch (e) { + showDataGridColumnChartErrorMessageToast(e, toastNotifications); + } + }; + + useEffect(() => { + if (dataGrid.chartsVisible) { + fetchColumnChartsData(); + } + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + dataGrid.chartsVisible, + jobConfig?.dest.index, + JSON.stringify([searchQuery, dataGrid.visibleColumns]), + ]); + const renderCellValue = useRenderCellValue( indexPattern, dataGrid.pagination, @@ -75,7 +109,6 @@ export const useExplorationResults = ( return { ...dataGrid, - columns, renderCellValue, }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 917ab1b0ed1dd..0b29b7f43bfc8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -53,7 +53,7 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery); - const { columns, errorMessage, status, tableItems } = outlierData; + const { columnsWithCharts, errorMessage, status, tableItems } = outlierData; // if it's a searchBar syntax error leave the table visible so they can try again if (status === INDEX_STATUS.ERROR && !errorMessage.includes('failed to create query')) { @@ -98,35 +98,36 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = )} - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && indexPattern !== undefined && ( - <> - - - - - - - 0 || searchQuery !== defaultSearchQuery) && + indexPattern !== undefined && ( + <> + + + + + + + + + + + {columnsWithCharts.length > 0 && tableItems.length > 0 && ( + - - - - {columns.length > 0 && tableItems.length > 0 && ( - - )} - - )} + )} + + )} ); }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index 37ab67e2a33cb..beb6836bf801f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -16,12 +16,16 @@ import { COLOR_RANGE_SCALE, } from '../../../../../components/color_range_legend'; import { + fetchChartsData, getDataGridSchemasFromFieldTypes, + showDataGridColumnChartErrorMessageToast, useDataGrid, useRenderCellValue, UseIndexDataReturnType, } from '../../../../../components/data_grid'; import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { ml } from '../../../../../services/ml_api_service'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; import { DEFAULT_RESULTS_FIELD, FEATURE_INFLUENCE } from '../../../../common/constants'; @@ -75,6 +79,34 @@ export const useOutlierData = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); + const fetchColumnChartsData = async function () { + try { + if (jobConfig !== undefined) { + const columnChartsData = await fetchChartsData( + jobConfig.dest.index, + ml.esSearch, + searchQuery, + columns.filter((cT) => dataGrid.visibleColumns.includes(cT.id)) + ); + dataGrid.setColumnCharts(columnChartsData); + } + } catch (e) { + showDataGridColumnChartErrorMessageToast(e, getToastNotifications()); + } + }; + + useEffect(() => { + if (dataGrid.chartsVisible) { + fetchColumnChartsData(); + } + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + dataGrid.chartsVisible, + jobConfig?.dest.index, + JSON.stringify([searchQuery, dataGrid.visibleColumns]), + ]); + const colorRange = useColorRange( COLOR_RANGE.BLUE, COLOR_RANGE_SCALE.INFLUENCER, @@ -115,7 +147,6 @@ export const useOutlierData = ( return { ...dataGrid, - columns, renderCellValue, }; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index e1aa27df5e979..c821c183ad370 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -6,10 +6,14 @@ import { useEffect } from 'react'; +import { EuiDataGridColumn } from '@elastic/eui'; + import { + fetchChartsData, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, getErrorMessage, + showDataGridColumnChartErrorMessageToast, useDataGrid, useRenderCellValue, EsSorting, @@ -23,6 +27,8 @@ import { isDefaultQuery, matchAllQuery, PivotQuery } from '../common'; import { SearchItems } from './use_search_items'; import { useApi } from './use_api'; +import { useToastNotifications } from '../app_dependencies'; + type IndexSearchResponse = SearchResponse7; export const useIndexData = ( @@ -30,11 +36,12 @@ export const useIndexData = ( query: PivotQuery ): UseIndexDataReturnType => { const api = useApi(); + const toastNotifications = useToastNotifications(); const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); // EuiDataGrid State - const columns = [ + const columns: EuiDataGridColumn[] = [ ...indexPatternFields.map((id) => { const field = indexPattern.fields.getByName(id); const schema = getDataGridSchemaFromKibanaFieldType(field); @@ -45,8 +52,10 @@ export const useIndexData = ( const dataGrid = useDataGrid(columns); const { + chartsVisible, pagination, resetPagination, + setColumnCharts, setErrorMessage, setRowCount, setStatus, @@ -61,7 +70,7 @@ export const useIndexData = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(query)]); - const getIndexData = async function () { + const fetchDataGridData = async function () { setErrorMessage(''); setStatus(INDEX_STATUS.LOADING); @@ -92,20 +101,43 @@ export const useIndexData = ( } catch (e) { setErrorMessage(getErrorMessage(e)); setStatus(INDEX_STATUS.ERROR); + return; + } + }; + + const fetchColumnChartsData = async function () { + try { + const columnChartsData = await fetchChartsData( + indexPattern.title, + api.esSearch, + isDefaultQuery(query) ? matchAllQuery : query, + columns.filter((cT) => dataGrid.visibleColumns.includes(cT.id)) + ); + + setColumnCharts(columnChartsData); + } catch (e) { + showDataGridColumnChartErrorMessageToast(e, toastNotifications); } }; useEffect(() => { - getIndexData(); + fetchDataGridData(); // custom comparison // eslint-disable-next-line react-hooks/exhaustive-deps }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + useEffect(() => { + if (chartsVisible) { + fetchColumnChartsData(); + } + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [chartsVisible, indexPattern.title, JSON.stringify([query, dataGrid.visibleColumns])]); + const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); return { ...dataGrid, - columns, renderCellValue, }; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts index 206dab173297b..a9f34996b9b51 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -7,6 +7,8 @@ import moment from 'moment-timezone'; import { useEffect, useMemo, useState } from 'react'; +import { EuiDataGridColumn } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; @@ -76,7 +78,7 @@ export const usePivotData = ( const api = useApi(); const aggsArr = useMemo(() => dictionaryToArray(aggs), [aggs]); - const groupByArr = dictionaryToArray(groupBy); + const groupByArr = useMemo(() => dictionaryToArray(groupBy), [groupBy]); // Filters mapping properties of type `object`, which get returned for nested field parents. const columnKeys = Object.keys(previewMappings.properties).filter( @@ -85,7 +87,7 @@ export const usePivotData = ( columnKeys.sort(sortColumns(groupByArr)); // EuiDataGrid State - const columns = columnKeys.map((id) => { + const columns: EuiDataGridColumn[] = columnKeys.map((id) => { const field = previewMappings.properties[id]; // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] @@ -195,8 +197,7 @@ export const usePivotData = ( }, [ indexPatternTitle, aggsArr, - JSON.stringify(groupByArr), - JSON.stringify(query), + JSON.stringify([groupByArr, query]), /* eslint-enable react-hooks/exhaustive-deps */ ]); @@ -251,7 +252,7 @@ export const usePivotData = ( return { ...dataGrid, - columns, + chartsButtonVisible: false, renderCellValue, }; }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index e183712b390cf..a917fc73ad8fb 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { useMemo, FC } from 'react'; import { DataGrid } from '../../../../../shared_imports'; @@ -24,14 +24,22 @@ interface ExpandedRowPreviewPaneProps { export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { const toastNotifications = useToastNotifications(); - const { aggList, groupByList, searchQuery } = applyTransformConfigToDefineState( - getDefaultStepDefineState({} as SearchItems), - transformConfig + + const { aggList, groupByList, searchQuery } = useMemo( + () => + applyTransformConfigToDefineState( + getDefaultStepDefineState({} as SearchItems), + transformConfig + ), + [transformConfig] ); - const pivotQuery = getPivotQuery(searchQuery); + + const pivotQuery = useMemo(() => getPivotQuery(searchQuery), [searchQuery]); + const indexPatternTitle = Array.isArray(transformConfig.source.index) ? transformConfig.source.index.join(',') : transformConfig.source.index; + const pivotPreviewProps = usePivotData(indexPatternTitle, pivotQuery, aggList, groupByList); return ( diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index b6650f08f12bd..e0bbcd0b5d9db 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -14,14 +14,17 @@ export { } from '../../../../src/plugins/es_ui_shared/public'; export { + fetchChartsData, getErrorMessage, extractErrorMessage, formatHumanReadableDateTimeSeconds, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, multiColumnSortFactory, + showDataGridColumnChartErrorMessageToast, useDataGrid, useRenderCellValue, + ChartData, DataGrid, EsSorting, RenderCellValue, diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index 76ce5e953e104..bf267c80cdcce 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -147,9 +147,25 @@ export default function ({ getService }: FtrProviderContext) { progress: '100', }, indexPreview: { - columns: 20, + columns: 10, rows: 5, }, + histogramCharts: [ + { chartAvailable: false, id: 'category', legend: 'Chart not supported.' }, + { chartAvailable: true, id: 'currency', legend: '1 category' }, + { + chartAvailable: false, + id: 'customer_birth_date', + legend: '0 documents contain field.', + }, + { chartAvailable: false, id: 'customer_first_name', legend: 'Chart not supported.' }, + { chartAvailable: false, id: 'customer_full_name', legend: 'Chart not supported.' }, + { chartAvailable: true, id: 'customer_gender', legend: '2 categories' }, + { chartAvailable: true, id: 'customer_id', legend: 'top 20 of 46 categories' }, + { chartAvailable: false, id: 'customer_last_name', legend: 'Chart not supported.' }, + { chartAvailable: true, id: 'customer_phone', legend: '1 category' }, + { chartAvailable: true, id: 'day_of_week', legend: '7 categories' }, + ], }, }, { @@ -229,9 +245,10 @@ export default function ({ getService }: FtrProviderContext) { progress: '100', }, indexPreview: { - columns: 20, + columns: 10, rows: 5, }, + histogramCharts: [], }, }, ]; @@ -289,6 +306,16 @@ export default function ({ getService }: FtrProviderContext) { await transform.wizard.assertAdvancedQueryEditorSwitchCheckState(false); }); + it('enables the index preview histogram charts', async () => { + await transform.wizard.enableIndexPreviewHistogramCharts(); + }); + + it('displays the index preview histogram charts', async () => { + await transform.wizard.assertIndexPreviewHistogramCharts( + testData.expected.histogramCharts + ); + }); + it('adds the group by entries', async () => { for (const [index, entry] of testData.groupByEntries.entries()) { await transform.wizard.assertGroupByInputExists(); @@ -323,6 +350,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('shows the pivot preview', async () => { + await transform.wizard.assertPivotPreviewChartHistogramButtonMissing(); await transform.wizard.assertPivotPreviewColumnValues( testData.expected.pivotPreview.column, testData.expected.pivotPreview.values diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index 8b61e8c895e30..9cfdbadac8a3b 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -76,6 +76,12 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail(selector); }, + async assertPivotPreviewChartHistogramButtonMissing() { + // the button should not exist because histogram charts + // for the pivot preview are not supported yet + await testSubjects.missingOrFail('transformPivotPreviewHistogramButton'); + }, + async parseEuiDataGrid(tableSubj: string) { const table = await testSubjects.find(`~${tableSubj}`); const $ = await table.parseDomContent(); @@ -155,6 +161,58 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await this.assertPivotPreviewExists('empty'); }, + async assertIndexPreviewHistogramChartButtonExists() { + await testSubjects.existOrFail('transformIndexPreviewHistogramButton'); + }, + + async enableIndexPreviewHistogramCharts() { + await this.assertIndexPreviewHistogramChartButtonCheckState(false); + await testSubjects.click('transformIndexPreviewHistogramButton'); + await this.assertIndexPreviewHistogramChartButtonCheckState(true); + }, + + async assertIndexPreviewHistogramChartButtonCheckState(expectedCheckState: boolean) { + const actualCheckState = + (await testSubjects.getAttribute( + 'transformIndexPreviewHistogramButton', + 'aria-checked' + )) === 'true'; + expect(actualCheckState).to.eql( + expectedCheckState, + `Chart histogram button check state should be '${expectedCheckState}' (got '${actualCheckState}')` + ); + }, + + async assertIndexPreviewHistogramCharts( + expectedHistogramCharts: Array<{ chartAvailable: boolean; id: string; legend: string }> + ) { + // For each chart, get the content of each header cell and assert + // the legend text and column id and if the chart should be present or not. + await retry.tryForTime(5000, async () => { + for (const [index, expected] of expectedHistogramCharts.entries()) { + await testSubjects.existOrFail(`mlDataGridChart-${index}`); + + if (expected.chartAvailable) { + await testSubjects.existOrFail(`mlDataGridChart-${index}-histogram`); + } else { + await testSubjects.missingOrFail(`mlDataGridChart-${index}-histogram`); + } + + const actualLegend = await testSubjects.getVisibleText(`mlDataGridChart-${index}-legend`); + expect(actualLegend).to.eql( + expected.legend, + `Legend text for column '${index}' should be '${expected.legend}' (got '${actualLegend}')` + ); + + const actualId = await testSubjects.getVisibleText(`mlDataGridChart-${index}-id`); + expect(actualId).to.eql( + expected.id, + `Id text for column '${index}' should be '${expected.id}' (got '${actualId}')` + ); + } + }); + }, + async assertQueryInputExists() { await testSubjects.existOrFail('transformQueryInput'); }, From bf4557321bfc0c5629d9456be70b6fd610d34b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 25 Jun 2020 11:28:50 +0200 Subject: [PATCH 22/82] [7.x][DOCS] Emphasizes where File Data Visualizer is located. (#69895) --- docs/images/data-viz-homepage.jpg | Bin 0 -> 180960 bytes docs/setup/connect-to-elasticsearch.asciidoc | 14 ++++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 docs/images/data-viz-homepage.jpg diff --git a/docs/images/data-viz-homepage.jpg b/docs/images/data-viz-homepage.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f7a952b65d41f430d75187c56924f397ae1e5cbd GIT binary patch literal 180960 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 UB6;=7asiI*H$WmANODj7Uoc1sH2?qr literal 0 HcmV?d00001 diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 8c04167de1236..0575b8532508f 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -18,10 +18,16 @@ to see all that you can do in {kib}. [[upload-data-kibana]] === Upload a CSV, JSON, or log file -To visualize data in a CSV, JSON, or log file, you can -upload it using the File Data Visualizer. On the home page, -click *Import a CSV, NDSON, or log file*, and then drag your file into the -File Data Visualizer. +experimental[] + +To visualize data in a CSV, JSON, or log file, you can upload it using the File +Data Visualizer. On the home page, click *Import a CSV, NDSON, or log file*, and +then drag your file into the File Data Visualizer. Alternatively, you can open +it by navigating to the Machine Learning app page from the sidebar menu and +selecting the Data Visualizer from the top navigation bar on the opening page. + +[role="screenshot"] +image::images/data-viz-homepage.jpg[File Data Visualizer on the home page] You can upload a file up to 100 MB. This value is configurable up to 1 GB in <>. From 9b61100cf9f9f2ea4a7792e906084f5ba36fa9ad Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Thu, 25 Jun 2020 11:52:03 +0200 Subject: [PATCH 23/82] adds kibana navigation tests (#69733) (#69746) --- .../cypress/integration/cases.spec.ts | 4 +- .../integration/cases_connectors.spec.ts | 4 +- .../cypress/integration/events_viewer.spec.ts | 12 +-- .../integration/fields_browser.spec.ts | 6 +- .../cypress/integration/inspect.spec.ts | 8 +- .../cypress/integration/navigation.spec.ts | 80 ++++++++++++++++--- .../cypress/integration/overview.spec.ts | 4 +- .../cypress/integration/search_bar.spec.ts | 4 +- .../timeline_data_providers.spec.ts | 4 +- .../timeline_flyout_button.spec.ts | 4 +- .../timeline_local_storage.spec.ts | 4 +- .../timeline_search_or_filter.spec.ts | 4 +- .../timeline_toggle_column.spec.ts | 4 +- .../cypress/integration/url_state.spec.ts | 4 +- .../cypress/screens/kibana_navigation.ts | 22 +++++ .../cypress/tasks/kibana_navigation.ts | 15 ++++ .../cypress/urls/navigation.ts | 12 +-- 17 files changed, 148 insertions(+), 47 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts create mode 100644 x-pack/plugins/security_solution/cypress/tasks/kibana_navigation.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts index bb2dffe8ddd7d..efd9ece8aec56 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts @@ -44,7 +44,7 @@ import { backToCases, createNewCase } from '../tasks/create_new_case'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; -import { CASES } from '../urls/navigation'; +import { CASES_URL } from '../urls/navigation'; describe('Cases', () => { before(() => { @@ -56,7 +56,7 @@ describe('Cases', () => { }); it('Creates a new case with timeline and opens the timeline', () => { - loginAndWaitForPageWithoutDateRange(CASES); + loginAndWaitForPageWithoutDateRange(CASES_URL); goToCreateNewCase(); createNewCase(case1); backToCases(); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts index 266d183ea1b85..ed885ad653e5d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts @@ -15,7 +15,7 @@ import { } from '../tasks/configure_cases'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { CASES } from '../urls/navigation'; +import { CASES_URL } from '../urls/navigation'; describe('Cases connectors', () => { before(() => { @@ -25,7 +25,7 @@ describe('Cases connectors', () => { }); it('Configures a new connector', () => { - loginAndWaitForPageWithoutDateRange(CASES); + loginAndWaitForPageWithoutDateRange(CASES_URL); goToEditExternalConnection(); openAddNewConnectorOption(); addServiceNowConnector(serviceNowConnector); diff --git a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts index b2d35f3f0c336..cd4573817cc27 100644 --- a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts @@ -33,7 +33,7 @@ import { } from '../tasks/hosts/events'; import { clearSearchBar, kqlSearch } from '../tasks/security_header'; -import { HOSTS_PAGE } from '../urls/navigation'; +import { HOSTS_URL } from '../urls/navigation'; import { resetFields } from '../tasks/timeline'; const defaultHeadersInDefaultEcsCategory = [ @@ -49,7 +49,7 @@ const defaultHeadersInDefaultEcsCategory = [ describe('Events Viewer', () => { context('Fields rendering', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); openEvents(); }); @@ -75,7 +75,7 @@ describe('Events Viewer', () => { context('Events viewer query modal', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); openEvents(); }); @@ -93,7 +93,7 @@ describe('Events Viewer', () => { context('Events viewer fields behaviour', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); openEvents(); }); @@ -124,7 +124,7 @@ describe('Events Viewer', () => { context('Events behaviour', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); openEvents(); waitsForEventsToBeLoaded(); }); @@ -155,7 +155,7 @@ describe('Events Viewer', () => { context.skip('Events columns', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); openEvents(); waitsForEventsToBeLoaded(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts index b0dbf94c0efb9..6438a738580b7 100644 --- a/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts @@ -31,7 +31,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { openTimeline } from '../tasks/security_main'; import { openTimelineFieldsBrowser, populateTimeline } from '../tasks/timeline'; -import { HOSTS_PAGE } from '../urls/navigation'; +import { HOSTS_URL } from '../urls/navigation'; const defaultHeaders = [ { id: '@timestamp' }, @@ -47,7 +47,7 @@ const defaultHeaders = [ describe('Fields Browser', () => { context('Fields Browser rendering', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); openTimeline(); populateTimeline(); openTimelineFieldsBrowser(); @@ -110,7 +110,7 @@ describe('Fields Browser', () => { context('Editing the timeline', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); openTimeline(); populateTimeline(); openTimelineFieldsBrowser(); diff --git a/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts index 0529c797ee07a..53ddff501db82 100644 --- a/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts @@ -19,12 +19,12 @@ import { openTimelineSettings, } from '../tasks/timeline'; -import { HOSTS_PAGE, NETWORK_PAGE } from '../urls/navigation'; +import { HOSTS_URL, NETWORK_URL } from '../urls/navigation'; describe('Inspect', () => { context('Hosts stats and tables', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); }); afterEach(() => { closesModal(); @@ -40,7 +40,7 @@ describe('Inspect', () => { context('Network stats and tables', () => { before(() => { - loginAndWaitForPage(NETWORK_PAGE); + loginAndWaitForPage(NETWORK_URL); }); afterEach(() => { closesModal(); @@ -57,7 +57,7 @@ describe('Inspect', () => { context('Timeline', () => { it('inspects the timeline', () => { const hostExistsQuery = 'host.name: *'; - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); openTimeline(); executeTimelineKQL(hostExistsQuery); openTimelineSettings(); diff --git a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts index 28ae42f8c0974..ea3a78c77152a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts @@ -16,45 +16,107 @@ import { import { loginAndWaitForPage } from '../tasks/login'; import { navigateFromHeaderTo } from '../tasks/security_header'; -import { TIMELINES_PAGE } from '../urls/navigation'; +import { + ALERTS_URL, + CASES_URL, + HOSTS_URL, + KIBANA_HOME, + MANAGEMENT_URL, + NETWORK_URL, + OVERVIEW_URL, + TIMELINES_URL, +} from '../urls/navigation'; +import { openKibanaNavigation, navigateFromKibanaCollapsibleTo } from '../tasks/kibana_navigation'; +import { + ALERTS_PAGE, + CASES_PAGE, + HOSTS_PAGE, + MANAGEMENT_PAGE, + NETWORK_PAGE, + OVERVIEW_PAGE, + TIMELINES_PAGE, +} from '../screens/kibana_navigation'; describe('top-level navigation common to all pages in the Security app', () => { before(() => { - loginAndWaitForPage(TIMELINES_PAGE); + loginAndWaitForPage(TIMELINES_URL); }); it('navigates to the Overview page', () => { navigateFromHeaderTo(OVERVIEW); - cy.url().should('include', '/security/overview'); + cy.url().should('include', OVERVIEW_URL); }); it('navigates to the Alerts page', () => { navigateFromHeaderTo(ALERTS); - cy.url().should('include', '/security/alerts'); + cy.url().should('include', ALERTS_URL); }); it('navigates to the Hosts page', () => { navigateFromHeaderTo(HOSTS); - cy.url().should('include', '/security/hosts'); + cy.url().should('include', HOSTS_URL); }); it('navigates to the Network page', () => { navigateFromHeaderTo(NETWORK); - cy.url().should('include', '/security/network'); + cy.url().should('include', NETWORK_URL); }); it('navigates to the Timelines page', () => { navigateFromHeaderTo(TIMELINES); - cy.url().should('include', '/security/timelines'); + cy.url().should('include', TIMELINES_URL); }); it('navigates to the Cases page', () => { navigateFromHeaderTo(CASES); - cy.url().should('include', '/security/cases'); + cy.url().should('include', CASES_URL); }); it('navigates to the Management page', () => { navigateFromHeaderTo(MANAGEMENT); - cy.url().should('include', '/security/management'); + cy.url().should('include', MANAGEMENT_URL); + }); +}); + +describe('Kibana navigation to all pages in the Security app ', () => { + before(() => { + loginAndWaitForPage(KIBANA_HOME); + }); + beforeEach(() => { + openKibanaNavigation(); + }); + it('navigates to the Overview page', () => { + navigateFromKibanaCollapsibleTo(OVERVIEW_PAGE); + cy.url().should('include', OVERVIEW_URL); + }); + + it('navigates to the Alerts page', () => { + navigateFromKibanaCollapsibleTo(ALERTS_PAGE); + cy.url().should('include', ALERTS_URL); + }); + + it('navigates to the Hosts page', () => { + navigateFromKibanaCollapsibleTo(HOSTS_PAGE); + cy.url().should('include', HOSTS_URL); + }); + + it('navigates to the Network page', () => { + navigateFromKibanaCollapsibleTo(NETWORK_PAGE); + cy.url().should('include', NETWORK_URL); + }); + + it('navigates to the Timelines page', () => { + navigateFromKibanaCollapsibleTo(TIMELINES_PAGE); + cy.url().should('include', TIMELINES_URL); + }); + + it('navigates to the Cases page', () => { + navigateFromKibanaCollapsibleTo(CASES_PAGE); + cy.url().should('include', CASES_URL); + }); + + it('navigates to the Management page', () => { + navigateFromKibanaCollapsibleTo(MANAGEMENT_PAGE); + cy.url().should('include', MANAGEMENT_URL); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts index 284deb67e1386..b799d487acd08 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts @@ -9,12 +9,12 @@ import { HOST_STATS, NETWORK_STATS } from '../screens/overview'; import { expandHostStats, expandNetworkStats } from '../tasks/overview'; import { loginAndWaitForPage } from '../tasks/login'; -import { OVERVIEW_PAGE } from '../urls/navigation'; +import { OVERVIEW_URL } from '../urls/navigation'; describe('Overview Page', () => { before(() => { cy.stubSecurityApi('overview'); - loginAndWaitForPage(OVERVIEW_PAGE); + loginAndWaitForPage(OVERVIEW_URL); }); it('Host stats render with correct values', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/search_bar.spec.ts b/x-pack/plugins/security_solution/cypress/integration/search_bar.spec.ts index 6428a855c8485..10759cc7de6e9 100644 --- a/x-pack/plugins/security_solution/cypress/integration/search_bar.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/search_bar.spec.ts @@ -9,13 +9,13 @@ import { openAddFilterPopover, fillAddFilterForm } from '../tasks/search_bar'; import { GLOBAL_SEARCH_BAR_FILTER_ITEM } from '../screens/search_bar'; import { hostIpFilter } from '../objects/filter'; -import { HOSTS_PAGE } from '../urls/navigation'; +import { HOSTS_URL } from '../urls/navigation'; import { waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; // FAILING: https://github.com/elastic/kibana/issues/69595 describe.skip('SearchBar', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); waitForAllHostsToBeLoaded(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts index 30d3dd22d8cd7..9a5c3e50aa82f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts @@ -22,11 +22,11 @@ import { loginAndWaitForPage } from '../tasks/login'; import { openTimeline } from '../tasks/security_main'; import { createNewTimeline } from '../tasks/timeline'; -import { HOSTS_PAGE } from '../urls/navigation'; +import { HOSTS_URL } from '../urls/navigation'; describe('timeline data providers', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); waitForAllHostsToBeLoaded(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts index e462d6ade5dc4..87639f41d4109 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_flyout_button.spec.ts @@ -11,11 +11,11 @@ import { loginAndWaitForPage } from '../tasks/login'; import { openTimeline, openTimelineIfClosed } from '../tasks/security_main'; import { createNewTimeline } from '../tasks/timeline'; -import { HOSTS_PAGE } from '../urls/navigation'; +import { HOSTS_URL } from '../urls/navigation'; describe('timeline flyout button', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); waitForAllHostsToBeLoaded(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts index a4352f58e6fc7..383ebe2220585 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts @@ -6,7 +6,7 @@ import { reload } from '../tasks/common'; import { loginAndWaitForPage } from '../tasks/login'; -import { HOSTS_PAGE } from '../urls/navigation'; +import { HOSTS_URL } from '../urls/navigation'; import { openEvents } from '../tasks/hosts/main'; import { DRAGGABLE_HEADER } from '../screens/timeline'; import { TABLE_COLUMN_EVENTS_MESSAGE } from '../screens/hosts/external_events'; @@ -15,7 +15,7 @@ import { removeColumn, resetFields } from '../tasks/timeline'; describe('persistent timeline', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); openEvents(); waitsForEventsToBeLoaded(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts index 00994f7a87a7b..a2e2a72a17946 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_search_or_filter.spec.ts @@ -10,11 +10,11 @@ import { loginAndWaitForPage } from '../tasks/login'; import { openTimeline } from '../tasks/security_main'; import { executeTimelineKQL } from '../tasks/timeline'; -import { HOSTS_PAGE } from '../urls/navigation'; +import { HOSTS_URL } from '../urls/navigation'; describe('timeline search or filter KQL bar', () => { beforeEach(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); }); it('executes a KQL query', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts index 841d41782b350..12e6f3db9b61e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts @@ -22,11 +22,11 @@ import { uncheckTimestampToggleField, } from '../tasks/timeline'; -import { HOSTS_PAGE } from '../urls/navigation'; +import { HOSTS_URL } from '../urls/navigation'; describe('toggle column in timeline', () => { before(() => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); }); beforeEach(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index 1cefa7fe73d35..53460d1dfcbc6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -38,7 +38,7 @@ import { executeTimelineKQL, } from '../tasks/timeline'; -import { HOSTS_PAGE } from '../urls/navigation'; +import { HOSTS_URL } from '../urls/navigation'; import { ABSOLUTE_DATE_RANGE } from '../urls/state'; const ABSOLUTE_DATE = { @@ -235,7 +235,7 @@ describe('url state', () => { }); it.skip('sets and reads the url state for timeline by id', () => { - loginAndWaitForPage(HOSTS_PAGE); + loginAndWaitForPage(HOSTS_URL); openTimeline(); executeTimelineKQL('host.name: *'); diff --git a/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts b/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts new file mode 100644 index 0000000000000..2f7956ce370bc --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ALERTS_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Alerts"]'; + +export const CASES_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Cases"]'; + +export const HOSTS_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Hosts"]'; + +export const KIBANA_NAVIGATION_TOGGLE = '[data-test-subj="toggleNavButton"]'; + +export const MANAGEMENT_PAGE = + '[data-test-subj="collapsibleNavGroup-security"] [title="Management"]'; + +export const NETWORK_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Network"]'; + +export const OVERVIEW_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Overview"]'; + +export const TIMELINES_PAGE = '[data-test-subj="collapsibleNavGroup-security"] [title="Timelines"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/kibana_navigation.ts b/x-pack/plugins/security_solution/cypress/tasks/kibana_navigation.ts new file mode 100644 index 0000000000000..2d5b5d0de39d2 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/kibana_navigation.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KIBANA_NAVIGATION_TOGGLE } from '../screens/kibana_navigation'; + +export const navigateFromKibanaCollapsibleTo = (page: string) => { + cy.get(page).click(); +}; + +export const openKibanaNavigation = () => { + cy.get(KIBANA_NAVIGATION_TOGGLE).click(); +}; diff --git a/x-pack/plugins/security_solution/cypress/urls/navigation.ts b/x-pack/plugins/security_solution/cypress/urls/navigation.ts index 7978aebfb413b..9da9abf388e4d 100644 --- a/x-pack/plugins/security_solution/cypress/urls/navigation.ts +++ b/x-pack/plugins/security_solution/cypress/urls/navigation.ts @@ -5,9 +5,9 @@ */ export const ALERTS_URL = 'app/security/alerts'; -export const CASES = '/app/security/cases'; +export const CASES_URL = '/app/security/cases'; export const DETECTIONS = '/app/siem#/detections'; -export const HOSTS_PAGE = '/app/security/hosts/allHosts'; +export const HOSTS_URL = '/app/security/hosts/allHosts'; export const HOSTS_PAGE_TAB_URLS = { allHosts: '/app/security/hosts/allHosts', anomalies: '/app/security/hosts/anomalies', @@ -15,6 +15,8 @@ export const HOSTS_PAGE_TAB_URLS = { events: '/app/security/hosts/events', uncommonProcesses: '/app/security/hosts/uncommonProcesses', }; -export const NETWORK_PAGE = '/app/security/network'; -export const OVERVIEW_PAGE = '/app/security/overview'; -export const TIMELINES_PAGE = '/app/security/timelines'; +export const KIBANA_HOME = '/app/home#/'; +export const MANAGEMENT_URL = '/app/security/management'; +export const NETWORK_URL = '/app/security/network'; +export const OVERVIEW_URL = '/app/security/overview'; +export const TIMELINES_URL = '/app/security/timelines'; From 0b23f8f938b143c88f28eeb97cc3fe17e5af903a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 25 Jun 2020 11:55:32 +0200 Subject: [PATCH 24/82] [7.x][ML] Changes the ML overview empty analytics panel text (#69804) --- .../overview/components/analytics_panel/analytics_panel.tsx | 2 +- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx index c379cd702daee..65e7ba9e8ab52 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx @@ -89,7 +89,7 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { body={

{i18n.translate('xpack.ml.overview.analyticsList.emptyPromptText', { - defaultMessage: `Data frame analytics enable you to perform different analyses of your data and annotates it with the results. The job puts the annotated data and a copy of the source data in a new index.`, + defaultMessage: `Data frame analytics enables you to perform outlier detection, regression, or classification analysis on your data and annotates it with the results. The job puts the annotated data and a copy of the source data in a new index.`, })}

} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bda29c0135ebe..b266a985e88ff 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10691,7 +10691,6 @@ "xpack.ml.newJob.wizard.validateJob.queryIsInvalidEsQuery": "データフィードクエリは有効な Elasticsearch クエリでなければなりません。", "xpack.ml.overview.analyticsList.createFirstJobMessage": "最初のデータフレーム分析ジョブを作成", "xpack.ml.overview.analyticsList.createJobButtonText": "ジョブを作成", - "xpack.ml.overview.analyticsList.emptyPromptText": "データフレーム分析は、様々なデータ分析を行い結果と共に注釈に追加することができます。ジョブは注釈付きデータと共に、ソースデータのコピーを新規インデックスに保存します。", "xpack.ml.overview.analyticsList.errorPromptTitle": "データフレーム分析リストの取得中にエラーが発生しました。", "xpack.ml.overview.analyticsList.id": "ID", "xpack.ml.overview.analyticsList.manageJobsButtonText": "ジョブの管理", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3f53130d00e75..ddd6114d8be26 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10695,7 +10695,6 @@ "xpack.ml.newJob.wizard.validateJob.queryIsInvalidEsQuery": "数据馈送查询必须是有效的 Elasticsearch 查询。", "xpack.ml.overview.analyticsList.createFirstJobMessage": "创建您的首个数据帧分析作业", "xpack.ml.overview.analyticsList.createJobButtonText": "创建作业", - "xpack.ml.overview.analyticsList.emptyPromptText": "数据帧分析允许您对数据执行不同的分析,并使用结果标注数据。该作业会将标注的数据以及源数据的副本置于新的索引中。", "xpack.ml.overview.analyticsList.errorPromptTitle": "获取数据帧分析列表时发生错误。", "xpack.ml.overview.analyticsList.id": "ID", "xpack.ml.overview.analyticsList.manageJobsButtonText": "管理作业", From bc2e63f928d7c27610400adc964710399e04c014 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 25 Jun 2020 13:51:21 +0200 Subject: [PATCH 25/82] add the `exactRoute` property to app registration (#69772) (#69897) * add the `exactRoute` property * add missing doc file * nits on jsdoc --- ...ibana-plugin-core-public.app.exactroute.md | 30 ++++++ .../public/kibana-plugin-core-public.app.md | 1 + .../application/application_service.tsx | 2 + .../integration_tests/router.test.tsx | 100 +++++++++++------- .../application/integration_tests/utils.tsx | 4 + src/core/public/application/types.ts | 19 ++++ .../application/ui/app_container.test.tsx | 2 + src/core/public/application/ui/app_router.tsx | 1 + src/core/public/public.api.md | 1 + 9 files changed, 122 insertions(+), 38 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.app.exactroute.md diff --git a/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md b/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md new file mode 100644 index 0000000000000..d1e0be17a92b2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [exactRoute](./kibana-plugin-core-public.app.exactroute.md) + +## App.exactRoute property + +If set to true, the application's route will only be checked against an exact match. Defaults to `false`. + +Signature: + +```typescript +exactRoute?: boolean; +``` + +## Example + + +```ts +core.application.register({ + id: 'my_app', + title: 'My App' + exactRoute: true, + mount: () => { ... }, +}) + +// '[basePath]/app/my_app' will be matched +// '[basePath]/app/my_app/some/path' will not be matched + +``` + diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index 90737d241f548..8dd60972549f9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -18,5 +18,6 @@ export interface App extends AppBase | --- | --- | --- | | [appRoute](./kibana-plugin-core-public.app.approute.md) | string | Override the application's routing path from /app/${id}. Must be unique across registered applications. Should not include the base path from HTTP. | | [chromeless](./kibana-plugin-core-public.app.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | +| [exactRoute](./kibana-plugin-core-public.app.exactroute.md) | boolean | If set to true, the application's route will only be checked against an exact match. Defaults to false. | | [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState> | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-core-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md). | diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 95361d8287c71..d7f15decb255d 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -201,6 +201,7 @@ export class ApplicationService { this.mounters.set(app.id, { appRoute: app.appRoute!, appBasePath: basePath.prepend(app.appRoute!), + exactRoute: app.exactRoute ?? false, mount: wrapMount(plugin, app), unmountBeforeMounting: false, legacy: false, @@ -236,6 +237,7 @@ export class ApplicationService { this.mounters.set(app.id, { appRoute, appBasePath, + exactRoute: false, mount, unmountBeforeMounting: true, legacy: true, diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index 2827b93f6d17e..f992e121437a9 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -30,7 +30,6 @@ import { ScopedHistory } from '../scoped_history'; describe('AppRouter', () => { let mounters: MockedMounterMap; let globalHistory: History; - let appStatuses$: BehaviorSubject>; let update: ReturnType; let scopedAppHistory: History; @@ -53,6 +52,17 @@ describe('AppRouter', () => { ); }; + const createMountersRenderer = () => + createRenderer( + + ); + beforeEach(() => { mounters = new Map([ createAppMounter({ appId: 'app1', html: 'App 1' }), @@ -90,16 +100,7 @@ describe('AppRouter', () => { }), ] as Array>); globalHistory = createMemoryHistory(); - appStatuses$ = mountersToAppStatus$(); - update = createRenderer( - - ); + update = createMountersRenderer(); }); it('calls mount handler and returned unmount function when navigating between apps', async () => { @@ -220,15 +221,7 @@ describe('AppRouter', () => { }) ); globalHistory = createMemoryHistory(); - update = createRenderer( - - ); + update = createMountersRenderer(); await navigate('/fake-login'); @@ -252,15 +245,7 @@ describe('AppRouter', () => { }) ); globalHistory = createMemoryHistory(); - update = createRenderer( - - ); + update = createMountersRenderer(); await navigate('/spaces/fake-login'); @@ -268,6 +253,53 @@ describe('AppRouter', () => { expect(mounters.get('login')!.mounter.mount).not.toHaveBeenCalled(); }); + it('should mount an exact route app only when the path is an exact match', async () => { + mounters.set( + ...createAppMounter({ + appId: 'exactApp', + html: '
exact app
', + exactRoute: true, + appRoute: '/app/exact-app', + }) + ); + + globalHistory = createMemoryHistory(); + update = createMountersRenderer(); + + await navigate('/app/exact-app/some-path'); + + expect(mounters.get('exactApp')!.mounter.mount).not.toHaveBeenCalled(); + + await navigate('/app/exact-app'); + + expect(mounters.get('exactApp')!.mounter.mount).toHaveBeenCalledTimes(1); + }); + + it('should mount an an app with a route nested in an exact route app', async () => { + mounters.set( + ...createAppMounter({ + appId: 'exactApp', + html: '
exact app
', + exactRoute: true, + appRoute: '/app/exact-app', + }) + ); + mounters.set( + ...createAppMounter({ + appId: 'nestedApp', + html: '
nested app
', + appRoute: '/app/exact-app/another-app', + }) + ); + globalHistory = createMemoryHistory(); + update = createMountersRenderer(); + + await navigate('/app/exact-app/another-app'); + + expect(mounters.get('exactApp')!.mounter.mount).not.toHaveBeenCalled(); + expect(mounters.get('nestedApp')!.mounter.mount).toHaveBeenCalledTimes(1); + }); + it('should not remount when changing pages within app', async () => { const { mounter, unmount } = mounters.get('app1')!; await navigate('/app/app1/page1'); @@ -304,15 +336,7 @@ describe('AppRouter', () => { it('should not remount when when changing pages within app using hash history', async () => { globalHistory = createHashHistory(); - update = createRenderer( - - ); + update = createMountersRenderer(); const { mounter, unmount } = mounters.get('app1')!; await navigate('/app/app1/page1'); diff --git a/src/core/public/application/integration_tests/utils.tsx b/src/core/public/application/integration_tests/utils.tsx index 8590fb3c820ef..80a7fc2c2cad6 100644 --- a/src/core/public/application/integration_tests/utils.tsx +++ b/src/core/public/application/integration_tests/utils.tsx @@ -47,11 +47,13 @@ export const createAppMounter = ({ appId, html = `
App ${appId}
`, appRoute = `/app/${appId}`, + exactRoute = false, extraMountHook, }: { appId: string; html?: string; appRoute?: string; + exactRoute?: boolean; extraMountHook?: (params: AppMountParameters) => void; }): MockedMounterTuple => { const unmount = jest.fn(); @@ -62,6 +64,7 @@ export const createAppMounter = ({ appRoute, appBasePath: appRoute, legacy: false, + exactRoute, mount: jest.fn(async (params: AppMountParameters) => { const { appBasePath: basename, element } = params; Object.assign(element, { @@ -90,6 +93,7 @@ export const createLegacyAppMounter = ( appBasePath: `/app/${appId.split(':')[0]}`, unmountBeforeMounting: true, legacy: true, + exactRoute: false, mount: legacyMount, }, unmount: jest.fn(), diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 44b095bd9e6d8..6926b6acf2411 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -234,6 +234,24 @@ export interface App extends AppBase { * base path from HTTP. */ appRoute?: string; + + /** + * If set to true, the application's route will only be checked against an exact match. Defaults to `false`. + * + * @example + * ```ts + * core.application.register({ + * id: 'my_app', + * title: 'My App' + * exactRoute: true, + * mount: () => { ... }, + * }) + * + * // '[basePath]/app/my_app' will be matched + * // '[basePath]/app/my_app/some/path' will not be matched + * ``` + */ + exactRoute?: boolean; } /** @public */ @@ -569,6 +587,7 @@ export type Mounter = SelectivePartial< appBasePath: string; mount: T extends LegacyApp ? LegacyAppMounter : AppMounter; legacy: boolean; + exactRoute: boolean; unmountBeforeMounting: T extends LegacyApp ? true : boolean; }, T extends LegacyApp ? never : 'unmountBeforeMounting' diff --git a/src/core/public/application/ui/app_container.test.tsx b/src/core/public/application/ui/app_container.test.tsx index 229354a014103..a94313dd53abb 100644 --- a/src/core/public/application/ui/app_container.test.tsx +++ b/src/core/public/application/ui/app_container.test.tsx @@ -55,6 +55,7 @@ describe('AppContainer', () => { appRoute: '/some-route', unmountBeforeMounting: false, legacy: false, + exactRoute: false, mount: async ({ element }: AppMountParameters) => { await promise; const container = document.createElement('div'); @@ -143,6 +144,7 @@ describe('AppContainer', () => { appRoute: '/some-route', unmountBeforeMounting: false, legacy: false, + exactRoute: false, mount: async ({ element }: AppMountParameters) => { await waitPromise; throw new Error(`Mounting failed!`); diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index 5d02f96134b27..f2d2d1e6587ac 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -63,6 +63,7 @@ export const AppRouter: FunctionComponent = ({ ( extends AppBase { appRoute?: string; chromeless?: boolean; + exactRoute?: boolean; mount: AppMount | AppMountDeprecated; } From 8665d5725ed51a1eb7e867a237fd322e8802582b Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Thu, 25 Jun 2020 08:35:19 -0400 Subject: [PATCH 26/82] [7.x] Convert Positionable, RenderToDom and RenderWithFn to functional/hooks/no recompose. (#68202) (#69866) Co-authored-by: Elastic Machine --- .../element_content/element_content.js | 12 +- .../element_wrapper/element_wrapper.js | 8 +- .../components/element_wrapper/index.js | 4 +- .../element_wrapper/lib/handlers.js | 60 ------- .../positionable/{index.js => index.ts} | 5 +- .../components/positionable/positionable.js | 42 ----- .../components/positionable/positionable.tsx | 48 ++++++ .../public/components/render_to_dom/index.js | 12 -- .../handlers.js => render_to_dom/index.ts} | 14 +- .../components/render_to_dom/render_to_dom.js | 40 ----- .../render_to_dom/render_to_dom.tsx | 27 +++ .../public/components/render_with_fn/index.js | 30 ---- .../public/components/render_with_fn/index.ts | 7 + .../render_with_fn/render_with_fn.js | 157 ------------------ .../render_with_fn/render_with_fn.tsx | 117 +++++++++++++ .../canvas/public/lib/create_handlers.ts | 96 +++++++++++ .../components/rendered_element.tsx | 14 +- x-pack/plugins/canvas/types/renderers.ts | 28 ++-- 18 files changed, 331 insertions(+), 390 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js rename x-pack/plugins/canvas/public/components/positionable/{index.js => index.ts} (63%) delete mode 100644 x-pack/plugins/canvas/public/components/positionable/positionable.js create mode 100644 x-pack/plugins/canvas/public/components/positionable/positionable.tsx delete mode 100644 x-pack/plugins/canvas/public/components/render_to_dom/index.js rename x-pack/plugins/canvas/public/components/{render_with_fn/lib/handlers.js => render_to_dom/index.ts} (61%) delete mode 100644 x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js create mode 100644 x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx delete mode 100644 x-pack/plugins/canvas/public/components/render_with_fn/index.js create mode 100644 x-pack/plugins/canvas/public/components/render_with_fn/index.ts delete mode 100644 x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.js create mode 100644 x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx create mode 100644 x-pack/plugins/canvas/public/lib/create_handlers.ts diff --git a/x-pack/plugins/canvas/public/components/element_content/element_content.js b/x-pack/plugins/canvas/public/components/element_content/element_content.js index 114a457d167e7..e2c1a61c348d1 100644 --- a/x-pack/plugins/canvas/public/components/element_content/element_content.js +++ b/x-pack/plugins/canvas/public/components/element_content/element_content.js @@ -12,6 +12,7 @@ import { getType } from '@kbn/interpreter/common'; import { Loading } from '../loading'; import { RenderWithFn } from '../render_with_fn'; import { ElementShareContainer } from '../element_share_container'; +import { assignHandlers } from '../../lib/create_handlers'; import { InvalidExpression } from './invalid_expression'; import { InvalidElementType } from './invalid_element_type'; @@ -46,7 +47,7 @@ const branches = [ export const ElementContent = compose( pure, ...branches -)(({ renderable, renderFunction, size, handlers }) => { +)(({ renderable, renderFunction, width, height, handlers }) => { const { getFilter, setFilter, @@ -62,7 +63,7 @@ export const ElementContent = compose(
diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js b/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js index 845fc5927d839..de7748413b718 100644 --- a/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js +++ b/x-pack/plugins/canvas/public/components/element_wrapper/element_wrapper.js @@ -14,7 +14,13 @@ export const ElementWrapper = (props) => { return ( - + ); }; diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/index.js b/x-pack/plugins/canvas/public/components/element_wrapper/index.js index 390c349ab2ee6..6fc582bfee444 100644 --- a/x-pack/plugins/canvas/public/components/element_wrapper/index.js +++ b/x-pack/plugins/canvas/public/components/element_wrapper/index.js @@ -10,12 +10,12 @@ import { compose, withPropsOnChange, mapProps } from 'recompose'; import isEqual from 'react-fast-compare'; import { getResolvedArgs, getSelectedPage } from '../../state/selectors/workpad'; import { getState, getValue } from '../../lib/resolved_arg'; +import { createDispatchedHandlerFactory } from '../../lib/create_handlers'; import { ElementWrapper as Component } from './element_wrapper'; -import { createHandlers as createHandlersWithDispatch } from './lib/handlers'; function selectorFactory(dispatch) { let result = {}; - const createHandlers = createHandlersWithDispatch(dispatch); + const createHandlers = createDispatchedHandlerFactory(dispatch); return (nextState, nextOwnProps) => { const { element, ...restOwnProps } = nextOwnProps; diff --git a/x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js b/x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js deleted file mode 100644 index 33e8eacd902dd..0000000000000 --- a/x-pack/plugins/canvas/public/components/element_wrapper/lib/handlers.js +++ /dev/null @@ -1,60 +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 { isEqual } from 'lodash'; -import { setFilter } from '../../../state/actions/elements'; -import { - updateEmbeddableExpression, - fetchEmbeddableRenderable, -} from '../../../state/actions/embeddable'; - -export const createHandlers = (dispatch) => { - let isComplete = false; - let oldElement; - let completeFn = () => {}; - - return (element) => { - // reset isComplete when element changes - if (!isEqual(oldElement, element)) { - isComplete = false; - oldElement = element; - } - - return { - setFilter(text) { - dispatch(setFilter(text, element.id, true)); - }, - - getFilter() { - return element.filter; - }, - - onComplete(fn) { - completeFn = fn; - }, - - getElementId: () => element.id, - - onEmbeddableInputChange(embeddableExpression) { - dispatch(updateEmbeddableExpression({ elementId: element.id, embeddableExpression })); - }, - - onEmbeddableDestroyed() { - dispatch(fetchEmbeddableRenderable(element.id)); - }, - - done() { - // don't emit if the element is already done - if (isComplete) { - return; - } - - isComplete = true; - completeFn(); - }, - }; - }; -}; diff --git a/x-pack/plugins/canvas/public/components/positionable/index.js b/x-pack/plugins/canvas/public/components/positionable/index.ts similarity index 63% rename from x-pack/plugins/canvas/public/components/positionable/index.js rename to x-pack/plugins/canvas/public/components/positionable/index.ts index e5c3c32acb024..964e2ee41df75 100644 --- a/x-pack/plugins/canvas/public/components/positionable/index.js +++ b/x-pack/plugins/canvas/public/components/positionable/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pure } from 'recompose'; -import { Positionable as Component } from './positionable'; - -export const Positionable = pure(Component); +export { Positionable } from './positionable'; diff --git a/x-pack/plugins/canvas/public/components/positionable/positionable.js b/x-pack/plugins/canvas/public/components/positionable/positionable.js deleted file mode 100644 index 9898f50cbb0f0..0000000000000 --- a/x-pack/plugins/canvas/public/components/positionable/positionable.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { matrixToCSS } from '../../lib/dom'; - -export const Positionable = ({ children, transformMatrix, width, height }) => { - // Throw if there is more than one child - React.Children.only(children); - // This could probably be made nicer by having just one child - const wrappedChildren = React.Children.map(children, (child) => { - const newStyle = { - width, - height, - marginLeft: -width / 2, - marginTop: -height / 2, - position: 'absolute', - transform: matrixToCSS(transformMatrix.map((n, i) => (i < 12 ? n : Math.round(n)))), - }; - - const stepChild = React.cloneElement(child, { size: { width, height } }); - return ( -
- {stepChild} -
- ); - }); - - return wrappedChildren; -}; - -Positionable.propTypes = { - onChange: PropTypes.func, - children: PropTypes.element.isRequired, - transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, -}; diff --git a/x-pack/plugins/canvas/public/components/positionable/positionable.tsx b/x-pack/plugins/canvas/public/components/positionable/positionable.tsx new file mode 100644 index 0000000000000..3344398b00198 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/positionable/positionable.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, ReactElement, CSSProperties } from 'react'; +import PropTypes from 'prop-types'; +import { matrixToCSS } from '../../lib/dom'; +import { TransformMatrix3d } from '../../lib/aeroelastic'; + +interface Props { + children: ReactElement; + transformMatrix: TransformMatrix3d; + height: number; + width: number; +} + +export const Positionable: FC = ({ children, transformMatrix, width, height }) => { + // Throw if there is more than one child + const childNode = React.Children.only(children); + + const matrix = (transformMatrix.map((n, i) => + i < 12 ? n : Math.round(n) + ) as any) as TransformMatrix3d; + + const newStyle: CSSProperties = { + width, + height, + marginLeft: -width / 2, + marginTop: -height / 2, + position: 'absolute', + transform: matrixToCSS(matrix), + }; + + return ( +
+ {childNode} +
+ ); +}; + +Positionable.propTypes = { + children: PropTypes.element.isRequired, + transformMatrix: PropTypes.arrayOf(PropTypes.number).isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/index.js b/x-pack/plugins/canvas/public/components/render_to_dom/index.js deleted file mode 100644 index e8a3f8cd8c93b..0000000000000 --- a/x-pack/plugins/canvas/public/components/render_to_dom/index.js +++ /dev/null @@ -1,12 +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 { compose, withState } from 'recompose'; -import { RenderToDom as Component } from './render_to_dom'; - -export const RenderToDom = compose( - withState('domNode', 'setDomNode') // Still don't like this, seems to be the only way todo it. -)(Component); diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/lib/handlers.js b/x-pack/plugins/canvas/public/components/render_to_dom/index.ts similarity index 61% rename from x-pack/plugins/canvas/public/components/render_with_fn/lib/handlers.js rename to x-pack/plugins/canvas/public/components/render_to_dom/index.ts index 9e5032efa97e2..43a5dad059c95 100644 --- a/x-pack/plugins/canvas/public/components/render_with_fn/lib/handlers.js +++ b/x-pack/plugins/canvas/public/components/render_to_dom/index.ts @@ -4,16 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export class ElementHandlers { - resize() {} - - destroy() {} - - onResize(fn) { - this.resize = fn; - } - - onDestroy(fn) { - this.destroy = fn; - } -} +export { RenderToDom } from './render_to_dom'; diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js b/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js deleted file mode 100644 index db393a8dde4f9..0000000000000 --- a/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; - -export class RenderToDom extends React.Component { - static propTypes = { - domNode: PropTypes.object, - setDomNode: PropTypes.func.isRequired, - render: PropTypes.func.isRequired, - style: PropTypes.object, - }; - - shouldComponentUpdate(nextProps) { - return this.props.domNode !== nextProps.domNode; - } - - componentDidUpdate() { - // Calls render function once we have the reference to the DOM element to render into - if (this.props.domNode) { - this.props.render(this.props.domNode); - } - } - - render() { - const { domNode, setDomNode, style } = this.props; - const linkRef = (refNode) => { - if (!domNode && refNode) { - // Initialize the domNode property. This should only happen once, even if config changes. - setDomNode(refNode); - } - }; - - return
; - } -} diff --git a/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx b/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx new file mode 100644 index 0000000000000..a37c0fc096e57 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/render_to_dom/render_to_dom.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, FC } from 'react'; +import CSS from 'csstype'; + +interface Props { + render: (element: HTMLElement) => void; + style?: CSS.Properties; +} + +export const RenderToDom: FC = ({ render, style }) => { + // https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node + const ref = useCallback( + (node: HTMLDivElement) => { + if (node !== null) { + render(node); + } + }, + [render] + ); + + return
; +}; diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/index.js b/x-pack/plugins/canvas/public/components/render_with_fn/index.js deleted file mode 100644 index 37c49624a3940..0000000000000 --- a/x-pack/plugins/canvas/public/components/render_with_fn/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { compose, withProps, withPropsOnChange } from 'recompose'; -import PropTypes from 'prop-types'; -import isEqual from 'react-fast-compare'; -import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { RenderWithFn as Component } from './render_with_fn'; -import { ElementHandlers } from './lib/handlers'; - -export const RenderWithFn = compose( - withPropsOnChange( - // rebuild elementHandlers when handlers object changes - (props, nextProps) => !isEqual(props.handlers, nextProps.handlers), - ({ handlers }) => ({ - handlers: Object.assign(new ElementHandlers(), handlers), - }) - ), - withKibana, - withProps((props) => ({ - onError: props.kibana.services.canvas.notify.error, - })) -)(Component); - -RenderWithFn.propTypes = { - handlers: PropTypes.object, -}; diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/index.ts b/x-pack/plugins/canvas/public/components/render_with_fn/index.ts new file mode 100644 index 0000000000000..4bfef734d34f4 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/render_with_fn/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 { RenderWithFn } from './render_with_fn'; diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.js b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.js deleted file mode 100644 index 305e7c81f98e5..0000000000000 --- a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.js +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { isEqual, cloneDeep } from 'lodash'; -import { RenderToDom } from '../render_to_dom'; -import { ErrorStrings } from '../../../i18n'; - -const { RenderWithFn: strings } = ErrorStrings; - -export class RenderWithFn extends React.Component { - static propTypes = { - name: PropTypes.string.isRequired, - renderFn: PropTypes.func.isRequired, - reuseNode: PropTypes.bool, - handlers: PropTypes.shape({ - // element handlers, see components/element_wrapper/lib/handlers.js - setFilter: PropTypes.func.isRequired, - getFilter: PropTypes.func.isRequired, - done: PropTypes.func.isRequired, - // render handlers, see lib/handlers.js - resize: PropTypes.func.isRequired, - onResize: PropTypes.func.isRequired, - destroy: PropTypes.func.isRequired, - onDestroy: PropTypes.func.isRequired, - }), - config: PropTypes.object.isRequired, - size: PropTypes.object.isRequired, - onError: PropTypes.func.isRequired, - }; - - static defaultProps = { - reuseNode: false, - }; - - componentDidMount() { - this.firstRender = true; - this.renderTarget = null; - } - - UNSAFE_componentWillReceiveProps({ renderFn }) { - const newRenderFunction = renderFn !== this.props.renderFn; - - if (newRenderFunction) { - this._resetRenderTarget(this._domNode); - } - } - - shouldComponentUpdate(prevProps) { - return !isEqual(this.props.size, prevProps.size) || this._shouldFullRerender(prevProps); - } - - componentDidUpdate(prevProps) { - const { handlers, size } = this.props; - // Config changes - if (this._shouldFullRerender(prevProps)) { - // This should be the only place you call renderFn besides the first time - this._callRenderFn(); - } - - // Size changes - if (!isEqual(size, prevProps.size)) { - return handlers.resize(size); - } - } - - componentWillUnmount() { - this.props.handlers.destroy(); - } - - _domNode = null; - - _callRenderFn = () => { - const { handlers, config, renderFn, reuseNode, name: functionName } = this.props; - // TODO: We should wait until handlers.done() is called before replacing the element content? - if (!reuseNode || !this.renderTarget) { - this._resetRenderTarget(this._domNode); - } - // else if (!firstRender) handlers.destroy(); - - const renderConfig = cloneDeep(config); - - // TODO: this is hacky, but it works. it stops Kibana from blowing up when a render throws - try { - renderFn(this.renderTarget, renderConfig, handlers); - this.firstRender = false; - } catch (err) { - console.error('renderFn threw', err); - this.props.onError(err, { title: strings.getRenderErrorMessage(functionName) }); - } - }; - - _resetRenderTarget = (domNode) => { - const { handlers } = this.props; - - if (!domNode) { - throw new Error('RenderWithFn can not reset undefined target node'); - } - - // call destroy on existing element - if (!this.firstRender) { - handlers.destroy(); - } - - while (domNode.firstChild) { - domNode.removeChild(domNode.firstChild); - } - - this.firstRender = true; - this.renderTarget = this._createRenderTarget(); - domNode.appendChild(this.renderTarget); - }; - - _createRenderTarget = () => { - const div = document.createElement('div'); - div.style.width = '100%'; - div.style.height = '100%'; - return div; - }; - - _shouldFullRerender = (prevProps) => { - // required to stop re-renders on element move, anything that should - // cause a re-render needs to be checked here - // TODO: fix props passed in to remove this check - return ( - this.props.handlers !== prevProps.handlers || - !isEqual(this.props.config, prevProps.config) || - !isEqual(this.props.renderFn.toString(), prevProps.renderFn.toString()) - ); - }; - - destroy = () => { - this.props.handlers.destroy(); - }; - - render() { - // NOTE: the data-shared-* attributes here are used for reporting - return ( -
- { - this._domNode = domNode; - this._callRenderFn(); - }} - /> -
- ); - } -} diff --git a/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx new file mode 100644 index 0000000000000..bc51128cf0c87 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/render_with_fn/render_with_fn.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect, useRef, FC, useCallback } from 'react'; +import { useDebounce } from 'react-use'; + +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { RenderToDom } from '../render_to_dom'; +import { ErrorStrings } from '../../../i18n'; +import { RendererHandlers } from '../../../types'; + +const { RenderWithFn: strings } = ErrorStrings; + +interface Props { + name: string; + renderFn: ( + domNode: HTMLElement, + config: Record, + handlers: RendererHandlers + ) => void | Promise; + reuseNode: boolean; + handlers: RendererHandlers; + config: Record; + height: number; + width: number; +} + +const style = { height: '100%', width: '100%' }; + +export const RenderWithFn: FC = ({ + name: functionName, + renderFn, + reuseNode = false, + handlers: incomingHandlers, + config, + width, + height, +}) => { + const { services } = useKibana(); + const onError = services.canvas.notify.error; + + const [domNode, setDomNode] = useState(null); + + // Tells us if the component is attempting to re-render into a previously-populated render target. + const firstRender = useRef(true); + // A reference to the node appended to the provided DOM node which is created and optionally replaced. + const renderTarget = useRef(null); + // A reference to the handlers, as the renderFn may mutate them, (via onXYZ functions) + const handlers = useRef(incomingHandlers); + + // Reset the render target, the node appended to the DOM node provided by RenderToDOM. + const resetRenderTarget = useCallback(() => { + if (!domNode) { + return; + } + + if (!firstRender) { + handlers.current.destroy(); + } + + while (domNode.firstChild) { + domNode.removeChild(domNode.firstChild); + } + + const div = document.createElement('div'); + div.style.width = '100%'; + div.style.height = '100%'; + domNode.appendChild(div); + + renderTarget.current = div; + firstRender.current = true; + }, [domNode]); + + useDebounce(() => handlers.current.resize({ height, width }), 150, [height, width]); + + useEffect( + () => () => { + handlers.current.destroy(); + }, + [] + ); + + const render = useCallback(() => { + renderFn(renderTarget.current!, config, handlers.current); + }, [renderTarget, config, renderFn]); + + useEffect(() => { + if (!domNode) { + return; + } + + if (!reuseNode || !renderTarget.current) { + resetRenderTarget(); + } + + try { + render(); + firstRender.current = false; + } catch (err) { + onError(err, { title: strings.getRenderErrorMessage(functionName) }); + } + }, [domNode, functionName, onError, render, resetRenderTarget, reuseNode]); + + return ( +
+ { + setDomNode(node); + }} + /> +
+ ); +}; diff --git a/x-pack/plugins/canvas/public/lib/create_handlers.ts b/x-pack/plugins/canvas/public/lib/create_handlers.ts new file mode 100644 index 0000000000000..4e0c7b217d5b7 --- /dev/null +++ b/x-pack/plugins/canvas/public/lib/create_handlers.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual } from 'lodash'; +// @ts-ignore untyped local +import { setFilter } from '../state/actions/elements'; +import { updateEmbeddableExpression, fetchEmbeddableRenderable } from '../state/actions/embeddable'; +import { RendererHandlers, CanvasElement } from '../../types'; + +// This class creates stub handlers to ensure every element and renderer fulfills the contract. +// TODO: consider warning if these methods are invoked but not implemented by the renderer...? + +export const createHandlers = (): RendererHandlers => ({ + destroy() {}, + done() {}, + event() {}, + getElementId() { + return ''; + }, + getFilter() { + return ''; + }, + onComplete(fn: () => void) { + this.done = fn; + }, + onDestroy(fn: () => void) { + this.destroy = fn; + }, + // TODO: these functions do not match the `onXYZ` and `xyz` pattern elsewhere. + onEmbeddableDestroyed() {}, + onEmbeddableInputChange() {}, + onResize(fn: (size: { height: number; width: number }) => void) { + this.resize = fn; + }, + reload() {}, + resize(_size: { height: number; width: number }) {}, + setFilter() {}, + update() {}, +}); + +export const assignHandlers = (handlers: Partial = {}): RendererHandlers => + Object.assign(createHandlers(), handlers); + +// TODO: this is a legacy approach we should unravel in the near future. +export const createDispatchedHandlerFactory = ( + dispatch: (action: any) => void +): ((element: CanvasElement) => RendererHandlers) => { + let isComplete = false; + let oldElement: CanvasElement | undefined; + let completeFn = () => {}; + + return (element: CanvasElement) => { + // reset isComplete when element changes + if (!isEqual(oldElement, element)) { + isComplete = false; + oldElement = element; + } + + return assignHandlers({ + setFilter(text: string) { + dispatch(setFilter(text, element.id, true)); + }, + + getFilter() { + return element.filter; + }, + + onComplete(fn: () => void) { + completeFn = fn; + }, + + getElementId: () => element.id, + + onEmbeddableInputChange(embeddableExpression: string) { + dispatch(updateEmbeddableExpression({ elementId: element.id, embeddableExpression })); + }, + + onEmbeddableDestroyed() { + dispatch(fetchEmbeddableRenderable(element.id)); + }, + + done() { + // don't emit if the element is already done + if (isComplete) { + return; + } + + isComplete = true; + completeFn(); + }, + }); + }; +}; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx b/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx index 5741f5f2d698c..6bcc0db92f1cc 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx @@ -7,13 +7,13 @@ import React, { FC, PureComponent } from 'react'; // @ts-expect-error untyped library import Style from 'style-it'; -// @ts-expect-error untyped local import { Positionable } from '../../public/components/positionable/positionable'; // @ts-expect-error untyped local import { elementToShape } from '../../public/components/workpad_page/utils'; import { CanvasRenderedElement } from '../types'; import { CanvasShareableContext, useCanvasShareableState } from '../context'; import { RendererSpec } from '../../types'; +import { createHandlers } from '../../public/lib/create_handlers'; import css from './rendered_element.module.scss'; @@ -62,17 +62,7 @@ export class RenderedElementComponent extends PureComponent { } try { - // TODO: These are stubbed, but may need implementation. - fn.render(this.ref.current, value.value, { - done: () => {}, - onDestroy: () => {}, - onResize: () => {}, - getElementId: () => '', - setFilter: () => {}, - getFilter: () => '', - onEmbeddableInputChange: () => {}, - onEmbeddableDestroyed: () => {}, - }); + fn.render(this.ref.current, value.value, createHandlers()); } catch (e) { // eslint-disable-next-line no-console console.log(as, e.message); diff --git a/x-pack/plugins/canvas/types/renderers.ts b/x-pack/plugins/canvas/types/renderers.ts index 2564b045d1cf7..772a16aa94c60 100644 --- a/x-pack/plugins/canvas/types/renderers.ts +++ b/x-pack/plugins/canvas/types/renderers.ts @@ -4,25 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -type GenericCallback = (callback: () => void) => void; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; -export interface RendererHandlers { - /** Handler to invoke when an element has finished rendering */ - done: () => void; +type GenericRendererCallback = (callback: () => void) => void; + +export interface RendererHandlers extends IInterpreterRenderHandlers { + /** Handler to invoke when an element should be destroyed. */ + destroy: () => void; /** Get the id of the element being rendered. Can be used as a unique ID in a render function */ getElementId: () => string; - /** Handler to invoke when an element is deleted or changes to a different render type */ - onDestroy: GenericCallback; - /** Handler to invoke when an element's dimensions have changed*/ - onResize: GenericCallback; /** Retrieves the value of the filter property on the element object persisted on the workpad */ getFilter: () => string; - /** Sets the value of the filter property on the element object persisted on the workpad */ - setFilter: (filter: string) => void; - /** Handler to invoke when the input to a function has changed internally */ - onEmbeddableInputChange: (expression: string) => void; + /** Handler to invoke when a renderer is considered complete */ + onComplete: (fn: () => void) => void; /** Handler to invoke when a rendered embeddable is destroyed */ onEmbeddableDestroyed: () => void; + /** Handler to invoke when the input to a function has changed internally */ + onEmbeddableInputChange: (expression: string) => void; + /** Handler to invoke when an element's dimensions have changed*/ + onResize: GenericRendererCallback; + /** Handler to invoke when an element should be resized. */ + resize: (size: { height: number; width: number }) => void; + /** Sets the value of the filter property on the element object persisted on the workpad */ + setFilter: (filter: string) => void; } export interface RendererSpec { From 0102fb80de5486f4ee56ce90693141ad45da1349 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 25 Jun 2020 09:05:22 -0400 Subject: [PATCH 27/82] Update Resolver generator script documentation (#69912) (#69918) --- .../scripts/endpoint/README.md | 50 ++----------------- 1 file changed, 4 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/security_solution/scripts/endpoint/README.md b/x-pack/plugins/security_solution/scripts/endpoint/README.md index 0c36a47307232..bd9502f2f59e0 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/README.md +++ b/x-pack/plugins/security_solution/scripts/endpoint/README.md @@ -13,52 +13,10 @@ Example command sequence to get ES and kibana running with sample data after ins `yarn es snapshot` -> starts ES -`npx yarn start --xpack.securitySolution.enabled=true --no-base-path` -> starts kibana +`npx yarn start --no-base-path` -> starts kibana. Note: you may need other configurations steps to start the security solution with endpoint support. -`cd ~/path/to/kibana/x-pack/plugins/endpoint` +`cd x-pack/plugins/security_solution/scripts/endpoint` -`yarn test:generate --auth elastic:changeme` -> run the resolver_generator.ts script +`yarn test:generate` -> run the resolver_generator.ts script -Resolver generator CLI options: - -```bash -Options: - --help Show help [boolean] - --seed, -s random seed to use for document generator - [string] - --node, -n elasticsearch node url - [string] [default: "http://elastic:changeme@localhost:9200"] - --kibana, -k kibana url - [string] [default: "http://elastic:changeme@localhost:5601"] - --eventIndex, --ei index to store events in - [string] [default: "events-endpoint-1"] - --metadataIndex, --mi index to store host metadata in - [string] [default: "metrics-endpoint.metadata-default-1"] - --policyIndex, --pi index to store host policy in - [string] [default: "metrics-endpoint.policy-default-1"] - --ancestors, --anc number of ancestors of origin to create - [number] [default: 3] - --generations, --gen number of child generations to create - [number] [default: 3] - --children, --ch maximum number of children per node - [number] [default: 3] - --relatedEvents, --related number of related events to create for each - process event [number] [default: 5] - --relatedAlerts, --relAlerts number of related alerts to create for each - process event [number] [default: 5] - --percentWithRelated, --pr percent of process events to add related events - and related alerts to [number] [default: 30] - --percentTerminated, --pt percent of process events to add termination - event for [number] [default: 30] - --maxChildrenPerNode, --maxCh always generate the max number of children per - node instead of it being random up to the max - children [boolean] [default: false] - --numHosts, --ne number of different hosts to generate alerts - for [number] [default: 1] - --numDocs, --nd number of metadata and policy response doc to - generate per host [number] [default: 5] - --alertsPerHost, --ape number of resolver trees to make for each host - [number] [default: 1] - --delete, -d delete indices and remake them - [boolean] [default: false] -``` +To see Resolver generator CLI options, run `yarn test:generate --help`. From b0f47d5751893beca9b306923e67459361ba8bdc Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 25 Jun 2020 09:16:28 -0400 Subject: [PATCH 28/82] [Ingest Manager] Kibana, not EPR, controls removable packages (#69761) (#69906) * Kibana, not EPR, controls removable packages * Add 'removable' property to OpenAPI PackageInfo schema * Undo changes to example /packages API output Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../common/openapi/spec_oas3.json | 195 ++++++++++++++++++ .../ingest_manager/common/types/models/epm.ts | 2 +- .../server/services/epm/packages/get.ts | 8 +- .../server/services/epm/packages/index.ts | 10 + .../server/services/epm/packages/install.ts | 5 +- 5 files changed, 212 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json index ea61d97145795..d17b4115e64ab 100644 --- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json +++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json @@ -1712,6 +1712,198 @@ }, "success": true } + }, + "required-package": { + "value": { + "response": { + "format_version": "1.0.0", + "name": "endpoint", + "title": "Elastic Endpoint", + "version": "0.3.0", + "readme": "/package/endpoint/0.3.0/docs/README.md", + "license": "basic", + "description": "This is the Elastic Endpoint package.", + "type": "solution", + "categories": [ + "security" + ], + "release": "beta", + "requirement": { + "kibana": { + "versions": ">7.4.0" + } + }, + "icons": [ + { + "src": "/package/endpoint/0.3.0/img/logo-endpoint-64-color.svg", + "size": "16x16", + "type": "image/svg+xml" + } + ], + "assets": { + "kibana": { + "dashboard": [ + { + "pkgkey": "endpoint-0.3.0", + "service": "kibana", + "type": "dashboard", + "file": "826759f0-7074-11ea-9bc8-6b38f4d29a16.json", + "path": "endpoint-0.3.0/kibana/dashboard/826759f0-7074-11ea-9bc8-6b38f4d29a16.json" + } + ], + "map": [ + { + "pkgkey": "endpoint-0.3.0", + "service": "kibana", + "type": "map", + "file": "a3a3bd10-706b-11ea-9bc8-6b38f4d29a16.json", + "path": "endpoint-0.3.0/kibana/map/a3a3bd10-706b-11ea-9bc8-6b38f4d29a16.json" + } + ], + "visualization": [ + { + "pkgkey": "endpoint-0.3.0", + "service": "kibana", + "type": "visualization", + "file": "1cfceda0-728b-11ea-9bc8-6b38f4d29a16.json", + "path": "endpoint-0.3.0/kibana/visualization/1cfceda0-728b-11ea-9bc8-6b38f4d29a16.json" + }, + { + "pkgkey": "endpoint-0.3.0", + "service": "kibana", + "type": "visualization", + "file": "1e525190-7074-11ea-9bc8-6b38f4d29a16.json", + "path": "endpoint-0.3.0/kibana/visualization/1e525190-7074-11ea-9bc8-6b38f4d29a16.json" + }, + { + "pkgkey": "endpoint-0.3.0", + "service": "kibana", + "type": "visualization", + "file": "55387750-729c-11ea-9bc8-6b38f4d29a16.json", + "path": "endpoint-0.3.0/kibana/visualization/55387750-729c-11ea-9bc8-6b38f4d29a16.json" + }, + { + "pkgkey": "endpoint-0.3.0", + "service": "kibana", + "type": "visualization", + "file": "92b1edc0-706a-11ea-9bc8-6b38f4d29a16.json", + "path": "endpoint-0.3.0/kibana/visualization/92b1edc0-706a-11ea-9bc8-6b38f4d29a16.json" + } + ] + } + }, + "datasets": [ + { + "id": "endpoint", + "title": "Endpoint Events", + "release": "experimental", + "type": "events", + "package": "endpoint", + "path": "events" + }, + { + "id": "endpoint.metadata", + "title": "Endpoint Metadata", + "release": "experimental", + "type": "metrics", + "package": "endpoint", + "path": "metadata" + }, + { + "id": "endpoint.policy", + "title": "Endpoint Policy Response", + "release": "experimental", + "type": "metrics", + "package": "endpoint", + "path": "policy" + }, + { + "id": "endpoint.telemetry", + "title": "Endpoint Telemetry", + "release": "experimental", + "type": "metrics", + "package": "endpoint", + "path": "telemetry" + } + ], + "datasources": [ + { + "name": "endpoint", + "title": "Endpoint data source", + "description": "Interact with the endpoint.", + "inputs": null, + "multiple": false + } + ], + "download": "/epr/endpoint/endpoint-0.3.0.tar.gz", + "path": "/package/endpoint/0.3.0", + "latestVersion": "0.3.0", + "removable": false, + "status": "installed", + "savedObject": { + "id": "endpoint", + "type": "epm-packages", + "updated_at": "2020-06-23T21:44:59.319Z", + "version": "Wzk4LDFd", + "attributes": { + "installed": [ + { + "id": "826759f0-7074-11ea-9bc8-6b38f4d29a16", + "type": "dashboard" + }, + { + "id": "1cfceda0-728b-11ea-9bc8-6b38f4d29a16", + "type": "visualization" + }, + { + "id": "1e525190-7074-11ea-9bc8-6b38f4d29a16", + "type": "visualization" + }, + { + "id": "55387750-729c-11ea-9bc8-6b38f4d29a16", + "type": "visualization" + }, + { + "id": "92b1edc0-706a-11ea-9bc8-6b38f4d29a16", + "type": "visualization" + }, + { + "id": "a3a3bd10-706b-11ea-9bc8-6b38f4d29a16", + "type": "map" + }, + { + "id": "events-endpoint", + "type": "index-template" + }, + { + "id": "metrics-endpoint.metadata", + "type": "index-template" + }, + { + "id": "metrics-endpoint.policy", + "type": "index-template" + }, + { + "id": "metrics-endpoint.telemetry", + "type": "index-template" + } + ], + "es_index_patterns": { + "events": "events-endpoint-*", + "metadata": "metrics-endpoint.metadata-*", + "policy": "metrics-endpoint.policy-*", + "telemetry": "metrics-endpoint.telemetry-*" + }, + "name": "endpoint", + "version": "0.3.0", + "internal": false, + "removable": false + }, + "references": [] + } + }, + "success": true + } } } } @@ -3822,6 +4014,9 @@ }, "path": { "type": "string" + }, + "removable": { + "type": "boolean" } }, "required": [ diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index cc9e23dc9388f..599165d2bfd98 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -58,7 +58,6 @@ export interface RegistryPackage { icons?: RegistryImage[]; assets?: string[]; internal?: boolean; - removable?: boolean; format_version: string; datasets?: Dataset[]; datasources?: RegistryDatasource[]; @@ -206,6 +205,7 @@ interface PackageAdditions { title: string; latestVersion: string; assets: AssetsGroupedByServiceByType; + removable?: boolean; } // Managers public HTTP response types diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index 7d5e6d6e88387..a261eec899d7c 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { Installation, InstallationStatus, PackageInfo, KibanaAssetType } from '../../../types'; import * as Registry from '../registry'; -import { createInstallableFrom } from './index'; +import { createInstallableFrom, isRequiredPackage } from './index'; export { fetchFile as getFile, SearchParams } from '../registry'; @@ -79,10 +79,7 @@ export async function getPackageInfo(options: { getInstallationObject({ savedObjectsClient, pkgName }), Registry.fetchFindLatestPackage(pkgName), Registry.getArchiveInfo(pkgName, pkgVersion), - ] as const); - // adding `as const` due to regression in TS 3.7.2 - // see https://github.com/microsoft/TypeScript/issues/34925#issuecomment-550021453 - // and https://github.com/microsoft/TypeScript/pull/33707#issuecomment-550718523 + ]); // add properties that aren't (or aren't yet) on Registry response const updated = { @@ -90,6 +87,7 @@ export async function getPackageInfo(options: { latestVersion: latestPackage.version, title: item.title || nameAsTitle(item.name), assets: Registry.groupPathsByService(assets || []), + removable: !isRequiredPackage(pkgName), }; return createInstallableFrom(updated, savedObject); } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index d49e0e661440f..b79f9178ad6af 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -26,6 +26,16 @@ export { export { installKibanaAssets, installPackage, ensureInstalledPackage } from './install'; export { removeInstallation } from './remove'; +type RequiredPackage = 'system' | 'endpoint'; +const requiredPackages: Record = { + system: true, + endpoint: true, +}; + +export function isRequiredPackage(value: string): value is RequiredPackage { + return value in requiredPackages; +} + export class PackageNotInstalledError extends Error { constructor(pkgkey: string) { super(`${pkgkey} is not installed`); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 736711f9152e9..910283549abdf 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -19,7 +19,7 @@ import { import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; import { getObject } from './get_objects'; -import { getInstallation, getInstallationObject } from './index'; +import { getInstallation, getInstallationObject, isRequiredPackage } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines } from '../elasticsearch/ingest_pipeline/install'; @@ -104,7 +104,8 @@ export async function installPackage(options: { throw Boom.badRequest('Cannot install or update to an out-of-date package'); const reinstall = pkgVersion === installedPkg?.attributes.version; - const { internal = false, removable = true } = registryPackageInfo; + const removable = !isRequiredPackage(pkgName); + const { internal = false } = registryPackageInfo; // delete the previous version's installation's SO kibana assets before installing new ones // in case some assets were removed in the new version From 4951124777e2f4ee67df3c89e09ba27cb4c31419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Thu, 25 Jun 2020 16:11:58 +0200 Subject: [PATCH 29/82] [7.x] [Logs UI] ML log integration splash screen (#69288) (#69916) Co-authored-by: Brandon Morelli Co-authored-by: Elastic Machine --- .../public/assets/anomaly_chart_minified.svg | 1 + .../logging/log_analysis_setup/index.ts | 1 + .../subscription_splash_content.tsx | 174 ++++++++++++++++++ .../infra/public/hooks/use_trial_status.tsx | 51 +++++ .../log_entry_categories/page_content.tsx | 4 +- .../logs/log_entry_rate/page_content.tsx | 4 +- .../infra/public/pages/logs/page_content.tsx | 10 +- 7 files changed, 232 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/infra/public/assets/anomaly_chart_minified.svg create mode 100644 x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx create mode 100644 x-pack/plugins/infra/public/hooks/use_trial_status.tsx diff --git a/x-pack/plugins/infra/public/assets/anomaly_chart_minified.svg b/x-pack/plugins/infra/public/assets/anomaly_chart_minified.svg new file mode 100644 index 0000000000000..dd1b39248bba2 --- /dev/null +++ b/x-pack/plugins/infra/public/assets/anomaly_chart_minified.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts index 7f2982f221a3c..72099e9b1b4b6 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/index.ts @@ -13,3 +13,4 @@ export * from './missing_results_privileges_prompt'; export * from './missing_setup_privileges_prompt'; export * from './ml_unavailable_prompt'; export * from './setup_status_unknown_prompt'; +export * from './subscription_splash_content'; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx new file mode 100644 index 0000000000000..e0e293b1cc3e7 --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/subscription_splash_content.tsx @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, + EuiText, + EuiButton, + EuiButtonEmpty, + EuiImage, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { LoadingPage } from '../../loading_page'; + +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { euiStyled } from '../../../../../observability/public'; +import { useTrialStatus } from '../../../hooks/use_trial_status'; + +export const SubscriptionSplashContent: React.FC = () => { + const { services } = useKibana(); + const { loadState, isTrialAvailable, checkTrialAvailability } = useTrialStatus(); + + useEffect(() => { + checkTrialAvailability(); + }, [checkTrialAvailability]); + + if (loadState === 'pending') { + return ( + + ); + } + + const canStartTrial = isTrialAvailable && loadState === 'resolved'; + + let title; + let description; + let cta; + + if (canStartTrial) { + title = ( + + ); + + description = ( + + ); + + cta = ( + + + + ); + } else { + title = ( + + ); + + description = ( + + ); + + cta = ( + + + + ); + } + + return ( + + + + + + +

{title}

+
+ + +

{description}

+
+ +
{cta}
+
+ + + +
+ + +

+ +

+
+ + + +
+
+
+
+ ); +}; + +const SubscriptionPage = euiStyled(EuiPage)` + height: 100% +`; + +const SubscriptionPageContent = euiStyled(EuiPageContent)` + max-width: 768px !important; +`; + +const SubscriptionPageFooter = euiStyled.div` + background: ${(props) => props.theme.eui.euiColorLightestShade}; + margin: 0 -${(props) => props.theme.eui.paddingSizes.l} -${(props) => + props.theme.eui.paddingSizes.l}; + padding: ${(props) => props.theme.eui.paddingSizes.l}; +`; diff --git a/x-pack/plugins/infra/public/hooks/use_trial_status.tsx b/x-pack/plugins/infra/public/hooks/use_trial_status.tsx new file mode 100644 index 0000000000000..9cc118d09c7e0 --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_trial_status.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { boolean } from 'io-ts'; +import { i18n } from '@kbn/i18n'; + +import { useState } from 'react'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { API_BASE_PATH as LICENSE_MANAGEMENT_API_BASE_PATH } from '../../../license_management/common/constants'; +import { useTrackedPromise } from '../utils/use_tracked_promise'; +import { decodeOrThrow } from '../../common/runtime_types'; + +interface UseTrialStatusState { + loadState: 'uninitialized' | 'pending' | 'resolved' | 'rejected'; + isTrialAvailable: boolean; + checkTrialAvailability: () => void; +} + +export function useTrialStatus(): UseTrialStatusState { + const { services } = useKibana(); + const [isTrialAvailable, setIsTrialAvailable] = useState(false); + + const [loadState, checkTrialAvailability] = useTrackedPromise( + { + createPromise: async () => { + const response = await services.http.get(`${LICENSE_MANAGEMENT_API_BASE_PATH}/start_trial`); + return decodeOrThrow(boolean)(response); + }, + onResolve: (response) => { + setIsTrialAvailable(response); + }, + onReject: (error) => { + services.notifications.toasts.addDanger( + i18n.translate('xpack.infra.trialStatus.trialStatusNetworkErrorMessage', { + defaultMessage: 'We could not determine if the trial license is available', + }) + ); + }, + }, + [services] + ); + + return { + loadState: loadState.state, + isTrialAvailable, + checkTrialAvailability, + }; +} diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index 04b472ceb59c8..5d9adb8a4f6ec 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -12,7 +12,7 @@ import { LogAnalysisSetupStatusUnknownPrompt, MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, - MlUnavailablePrompt, + SubscriptionSplashContent, } from '../../../components/logging/log_analysis_setup'; import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; @@ -50,7 +50,7 @@ export const LogEntryCategoriesPageContent = () => { } else if (hasFailedLoadingSource) { return ; } else if (!hasLogAnalysisCapabilites) { - return ; + return ; } else if (!hasLogAnalysisReadCapabilities) { return ; } else if (setupStatus.type === 'initializing') { diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index fc07289f02fe7..4ec05a9778512 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -12,7 +12,7 @@ import { LogAnalysisSetupStatusUnknownPrompt, MissingResultsPrivilegesPrompt, MissingSetupPrivilegesPrompt, - MlUnavailablePrompt, + SubscriptionSplashContent, } from '../../../components/logging/log_analysis_setup'; import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; @@ -50,7 +50,7 @@ export const LogEntryRatePageContent = () => { } else if (hasFailedLoadingSource) { return ; } else if (!hasLogAnalysisCapabilites) { - return ; + return ; } else if (!hasLogAnalysisReadCapabilities) { return ; } else if (setupStatus.type === 'initializing') { diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index 78b7f86993cbd..c5047dbdf3bb5 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -17,7 +17,6 @@ import { HelpCenterContent } from '../../components/help_center_content'; import { AppNavigation } from '../../components/navigation/app_navigation'; import { RoutedTabs } from '../../components/navigation/routed_tabs'; import { ColumnarPage } from '../../components/page'; -import { useLogAnalysisCapabilitiesContext } from '../../containers/logs/log_analysis'; import { useLogSourceContext } from '../../containers/logs/log_source'; import { RedirectWithQueryParams } from '../../utils/redirect_with_query_params'; import { LogEntryCategoriesPage } from './log_entry_categories'; @@ -28,7 +27,6 @@ import { AlertDropdown } from '../../components/alerting/logs/alert_dropdown'; export const LogsPageContent: React.FunctionComponent = () => { const uiCapabilities = useKibana().services.application?.capabilities; - const logAnalysisCapabilities = useLogAnalysisCapabilitiesContext(); const { initialize } = useLogSourceContext(); @@ -79,13 +77,7 @@ export const LogsPageContent: React.FunctionComponent = () => { - + From 10a3c336d6b28b319c99d4c0be3b678ca72e5cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 25 Jun 2020 16:26:19 +0200 Subject: [PATCH 30/82] [7.x][ML] Changes View results button text on new job page. (#69913) --- .../new_job/recognize/components/create_result_callout.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx index 4602ceeec905f..6b2048f062f0f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/create_result_callout.tsx @@ -86,12 +86,12 @@ export const CreateResultCallout: FC = memo( fill={true} href={resultsUrl} aria-label={i18n.translate('xpack.ml.newJob.recognize.viewResultsAriaLabel', { - defaultMessage: 'View Results', + defaultMessage: 'View results', })} > From ce7dae03b6bb709e0827b48f42e183bf91163038 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Thu, 25 Jun 2020 16:28:44 +0200 Subject: [PATCH 31/82] unskips 'Events columns' test (#69684) (#69901) --- .../security_solution/cypress/integration/events_viewer.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts index cd4573817cc27..84ca1e20e9576 100644 --- a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts @@ -153,7 +153,7 @@ describe('Events Viewer', () => { }); }); - context.skip('Events columns', () => { + context('Events columns', () => { before(() => { loginAndWaitForPage(HOSTS_URL); openEvents(); From 68c3d79e12c46f31a660a3336c0d356d6779e836 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 25 Jun 2020 10:48:10 -0400 Subject: [PATCH 32/82] [ML] DF Analytics: Creation wizard part 3 (#69456) (#69920) * update clone tests * validate advanced params with explain * disable button while fetching validation data * comment out clone tests for now --- .../data_frame_analytics/common/analytics.ts | 7 + .../advanced_step/advanced_step_form.tsx | 77 +++++++++- .../advanced_step/hyper_parameters.tsx | 25 +++- .../outlier_hyper_parameters.tsx | 16 +- .../analysis_fields_table.tsx | 2 +- .../configuration_step_form.tsx | 140 ++++++++---------- .../components/shared/fetch_explain_data.ts | 48 ++++++ .../components/shared/index.ts | 1 + .../pages/analytics_creation/page.tsx | 2 +- .../use_create_analytics_form/reducer.ts | 17 +-- .../hooks/use_create_analytics_form/state.ts | 19 +-- .../apps/ml/data_frame_analytics/cloning.ts | 41 +++-- .../ml/data_frame_analytics_creation.ts | 59 +++----- .../services/ml/data_frame_analytics_table.ts | 2 +- 14 files changed, 284 insertions(+), 172 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 16d888a9da27b..ac455120dca83 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -25,11 +25,18 @@ export enum ANALYSIS_CONFIG_TYPE { } export enum ANALYSIS_ADVANCED_FIELDS { + ETA = 'eta', + FEATURE_BAG_FRACTION = 'feature_bag_fraction', FEATURE_INFLUENCE_THRESHOLD = 'feature_influence_threshold', GAMMA = 'gamma', LAMBDA = 'lambda', MAX_TREES = 'max_trees', + METHOD = 'method', + N_NEIGHBORS = 'n_neighbors', + NUM_TOP_CLASSES = 'num_top_classes', NUM_TOP_FEATURE_IMPORTANCE_VALUES = 'num_top_feature_importance_values', + OUTLIER_FRACTION = 'outlier_fraction', + RANDOMIZE_SEED = 'randomize_seed', } export enum OUTLIER_ANALYSIS_METHOD { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx index 8b137ac72361c..bc9bb0cce5ae8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useMemo } from 'react'; +import React, { FC, Fragment, useMemo, useEffect, useState } from 'react'; import { EuiAccordion, EuiFieldNumber, @@ -23,9 +23,11 @@ import { getModelMemoryLimitErrors } from '../../../analytics_management/hooks/u import { ANALYSIS_CONFIG_TYPE, NUM_TOP_FEATURE_IMPORTANCE_VALUES_MIN, + ANALYSIS_ADVANCED_FIELDS, } from '../../../../common/analytics'; import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../analytics_management/hooks/use_create_analytics_form/state'; import { ANALYTICS_STEPS } from '../../page'; +import { fetchExplainData } from '../shared'; import { ContinueButton } from '../continue_button'; import { OutlierHyperParameters } from './outlier_hyper_parameters'; @@ -33,23 +35,39 @@ export function getNumberValue(value?: number) { return value === undefined ? '' : +value; } +export type AdvancedParamErrors = { + [key in ANALYSIS_ADVANCED_FIELDS]?: string; +}; + export const AdvancedStepForm: FC = ({ actions, state, setCurrentStep, }) => { + const [advancedParamErrors, setAdvancedParamErrors] = useState({}); + const [fetchingAdvancedParamErrors, setFetchingAdvancedParamErrors] = useState(false); + const { setFormState } = actions; const { form, isJobCreated } = state; const { computeFeatureInfluence, + eta, + featureBagFraction, featureInfluenceThreshold, + gamma, jobType, + lambda, + maxTrees, + method, modelMemoryLimit, modelMemoryLimitValidationResult, + nNeighbors, numTopClasses, numTopFeatureImportanceValues, numTopFeatureImportanceValuesValid, + outlierFraction, predictionFieldName, + randomizeSeed, } = form; const mmlErrors = useMemo(() => getModelMemoryLimitErrors(modelMemoryLimitValidationResult), [ @@ -61,6 +79,43 @@ export const AdvancedStepForm: FC = ({ const mmlInvalid = modelMemoryLimitValidationResult !== null; + const isStepInvalid = + mmlInvalid || + Object.keys(advancedParamErrors).length > 0 || + fetchingAdvancedParamErrors === true; + + useEffect(() => { + setFetchingAdvancedParamErrors(true); + (async function () { + const { success, errorMessage } = await fetchExplainData(form); + const paramErrors: AdvancedParamErrors = {}; + + if (!success) { + // Check which field is invalid + Object.values(ANALYSIS_ADVANCED_FIELDS).forEach((param) => { + if (errorMessage.includes(`[${param}]`)) { + paramErrors[param] = errorMessage; + } + }); + } + setFetchingAdvancedParamErrors(false); + setAdvancedParamErrors(paramErrors); + })(); + }, [ + eta, + featureBagFraction, + featureInfluenceThreshold, + gamma, + lambda, + maxTrees, + method, + nNeighbors, + numTopClasses, + numTopFeatureImportanceValues, + outlierFraction, + randomizeSeed, + ]); + const outlierDetectionAdvancedConfig = ( @@ -126,6 +181,10 @@ export const AdvancedStepForm: FC = ({ 'The minimum outlier score that a document needs to have in order to calculate its feature influence score. Value range: 0-1. Defaults to 0.1.', } )} + isInvalid={ + advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.FEATURE_INFLUENCE_THRESHOLD] !== undefined + } + error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.FEATURE_INFLUENCE_THRESHOLD]} > @@ -315,14 +374,24 @@ export const AdvancedStepForm: FC = ({ > {jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && ( - + + )} + {isRegOrClassJob && ( + )} - {isRegOrClassJob && } { setCurrentStep(ANALYTICS_STEPS.DETAILS); }} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx index 144a062106003..620e81e30a0c4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx @@ -8,11 +8,16 @@ import React, { FC, Fragment } from 'react'; import { EuiFieldNumber, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { CreateAnalyticsFormProps } from '../../../analytics_management/hooks/use_create_analytics_form'; -import { getNumberValue } from './advanced_step_form'; +import { AdvancedParamErrors, getNumberValue } from './advanced_step_form'; +import { ANALYSIS_ADVANCED_FIELDS } from '../../../../common/analytics'; const MAX_TREES_LIMIT = 2000; -export const HyperParameters: FC = ({ actions, state }) => { +interface Props extends CreateAnalyticsFormProps { + advancedParamErrors: AdvancedParamErrors; +} + +export const HyperParameters: FC = ({ actions, state, advancedParamErrors }) => { const { setFormState } = actions; const { eta, featureBagFraction, gamma, lambda, maxTrees, randomizeSeed } = state.form; @@ -28,6 +33,8 @@ export const HyperParameters: FC = ({ actions, state } defaultMessage: 'Regularization parameter to prevent overfitting on the training data set. Must be a non negative value.', })} + isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.LAMBDA] !== undefined} + error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.LAMBDA]} > = ({ actions, state } helpText={i18n.translate('xpack.ml.dataframe.analytics.create.maxTreesText', { defaultMessage: 'The maximum number of trees the forest is allowed to contain.', })} + isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.MAX_TREES] !== undefined} + error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.MAX_TREES]} > = ({ actions, state } defaultMessage: 'Multiplies a linear penalty associated with the size of individual trees in the forest. Must be non-negative value.', })} + isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.GAMMA] !== undefined} + error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.GAMMA]} > = ({ actions, state } helpText={i18n.translate('xpack.ml.dataframe.analytics.create.etaText', { defaultMessage: 'The shrinkage applied to the weights. Must be between 0.001 and 1.', })} + isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.ETA] !== undefined} + error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.ETA]} > = ({ actions, state } defaultMessage: 'The fraction of features used when selecting a random bag for each candidate split.', })} + isInvalid={ + advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.FEATURE_BAG_FRACTION] !== undefined + } + error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.FEATURE_BAG_FRACTION]} > = ({ actions, state } = ({ actions, state }) => { +interface Props extends CreateAnalyticsFormProps { + advancedParamErrors: AdvancedParamErrors; +} + +export const OutlierHyperParameters: FC = ({ actions, state, advancedParamErrors }) => { const { setFormState } = actions; const { method, nNeighbors, outlierFraction, standardizationEnabled } = state.form; @@ -27,6 +31,8 @@ export const OutlierHyperParameters: FC = ({ actions, defaultMessage: 'Sets the method that outlier detection uses. If not set, uses an ensemble of different methods and normalises and combines their individual outlier scores to obtain the overall outlier score. We recommend to use the ensemble method', })} + isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.METHOD] !== undefined} + error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.METHOD]} > ({ @@ -51,6 +57,8 @@ export const OutlierHyperParameters: FC = ({ actions, defaultMessage: 'The value for how many nearest neighbors each method of outlier detection will use to calculate its outlier score. When not set, different values will be used for different ensemble members. Must be a positive integer', })} + isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.N_NEIGHBORS] !== undefined} + error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.N_NEIGHBORS]} > = ({ actions, defaultMessage: 'Sets the proportion of the data set that is assumed to be outlying prior to outlier detection.', })} + isInvalid={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.OUTLIER_FRACTION] !== undefined} + error={advancedParamErrors[ANALYSIS_ADVANCED_FIELDS.OUTLIER_FRACTION]} > )} {tableItems.length > 0 && ( - + = ({ const { currentSavedSearch, currentIndexPattern } = mlContext; const { savedSearchQuery, savedSearchQueryStr } = useSavedSearch(); + const [loadingFieldOptions, setLoadingFieldOptions] = useState(false); + const [fieldOptionsFetchFail, setFieldOptionsFetchFail] = useState(false); + const [loadingDepVarOptions, setLoadingDepVarOptions] = useState(false); + const [dependentVariableFetchFail, setDependentVariableFetchFail] = useState(false); + const [dependentVariableOptions, setDependentVariableOptions] = useState< + EuiComboBoxOptionOption[] + >([]); + const [excludesTableItems, setExcludesTableItems] = useState([]); + const [maxDistinctValuesError, setMaxDistinctValuesError] = useState( + undefined + ); + const { setEstimatedModelMemoryLimit, setFormState } = actions; const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state; const firstUpdate = useRef(true); const { dependentVariable, - dependentVariableFetchFail, - dependentVariableOptions, excludes, - excludesTableItems, - fieldOptionsFetchFail, jobConfigQuery, jobConfigQueryString, jobType, - loadingDepVarOptions, - loadingFieldOptions, - maxDistinctValuesError, modelMemoryLimit, previousJobType, requiredFieldsError, @@ -109,30 +120,20 @@ export const ConfigurationStepForm: FC = ({ requiredFieldsError !== undefined; const loadDepVarOptions = async (formState: State['form']) => { - setFormState({ - loadingDepVarOptions: true, - maxDistinctValuesError: undefined, - }); + setLoadingDepVarOptions(true); + setMaxDistinctValuesError(undefined); + try { if (currentIndexPattern !== undefined) { - const formStateUpdate: { - loadingDepVarOptions: boolean; - dependentVariableFetchFail: boolean; - dependentVariableOptions: State['form']['dependentVariableOptions']; - dependentVariable?: State['form']['dependentVariable']; - } = { - loadingDepVarOptions: false, - dependentVariableFetchFail: false, - dependentVariableOptions: [] as State['form']['dependentVariableOptions'], - }; - + const depVarOptions = []; + let depVarUpdate = dependentVariable; // Get fields and filter for supported types for job type const { fields } = newJobCapsService; let resetDependentVariable = true; for (const field of fields) { if (shouldAddAsDepVarOption(field, jobType)) { - formStateUpdate.dependentVariableOptions.push({ + depVarOptions.push({ label: field.id, }); @@ -143,13 +144,16 @@ export const ConfigurationStepForm: FC = ({ } if (resetDependentVariable) { - formStateUpdate.dependentVariable = ''; + depVarUpdate = ''; } - - setFormState(formStateUpdate); + setDependentVariableOptions(depVarOptions); + setLoadingDepVarOptions(false); + setDependentVariableFetchFail(false); + setFormState({ dependentVariable: depVarUpdate }); } } catch (e) { - setFormState({ loadingDepVarOptions: false, dependentVariableFetchFail: true }); + setLoadingDepVarOptions(false); + setDependentVariableFetchFail(true); } }; @@ -165,72 +169,48 @@ export const ConfigurationStepForm: FC = ({ // Reset if jobType changes (jobType requires dependent_variable to be set - // which won't be the case if switching from outlier detection) if (jobTypeChanged) { - setFormState({ - loadingFieldOptions: true, - }); + setLoadingFieldOptions(true); } - try { - const jobConfig = getJobConfigFromFormState(form); - delete jobConfig.dest; - delete jobConfig.model_memory_limit; - const resp: DfAnalyticsExplainResponse = await ml.dataFrameAnalytics.explainDataFrameAnalytics( - jobConfig - ); - const expectedMemoryWithoutDisk = resp.memory_estimation?.expected_memory_without_disk; + const { success, expectedMemory, fieldSelection, errorMessage } = await fetchExplainData(form); + if (success) { if (shouldUpdateEstimatedMml) { - setEstimatedModelMemoryLimit(expectedMemoryWithoutDisk); + setEstimatedModelMemoryLimit(expectedMemory); } - const fieldSelection: FieldSelectionItem[] | undefined = resp.field_selection; - - let hasRequiredFields = false; - if (fieldSelection) { - for (let i = 0; i < fieldSelection.length; i++) { - const field = fieldSelection[i]; - if (field.is_included === true && field.is_required === false) { - hasRequiredFields = true; - break; - } - } - } + const hasRequiredFields = fieldSelection.some( + (field) => field.is_included === true && field.is_required === false + ); - // If job type has changed load analysis field options again if (jobTypeChanged) { + setLoadingFieldOptions(false); + setFieldOptionsFetchFail(false); + setMaxDistinctValuesError(undefined); + setExcludesTableItems(fieldSelection ? fieldSelection : []); setFormState({ - ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}), - excludesTableItems: fieldSelection ? fieldSelection : [], - loadingFieldOptions: false, - fieldOptionsFetchFail: false, - maxDistinctValuesError: undefined, + ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemory } : {}), requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, }); } else { setFormState({ - ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemoryWithoutDisk } : {}), + ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemory } : {}), requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, }); } - } catch (e) { + } else { let maxDistinctValuesErrorMessage; - if ( jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && - e.body && - e.body.message !== undefined && - e.body.message.includes('status_exception') && - (e.body.message.includes('must have at most') || - e.body.message.includes('must have at least')) + errorMessage.includes('status_exception') && + (errorMessage.includes('must have at most') || errorMessage.includes('must have at least')) ) { - maxDistinctValuesErrorMessage = e.body.message; + maxDistinctValuesErrorMessage = errorMessage; } if ( - e.body && - e.body.message !== undefined && - e.body.message.includes('status_exception') && - e.body.message.includes('Unable to estimate memory usage as no documents') + errorMessage.includes('status_exception') && + errorMessage.includes('Unable to estimate memory usage as no documents') ) { toastNotifications.addWarning( i18n.translate('xpack.ml.dataframe.analytics.create.allDocsMissingFieldsErrorMessage', { @@ -241,15 +221,17 @@ export const ConfigurationStepForm: FC = ({ }) ); } + const fallbackModelMemoryLimit = jobType !== undefined ? DEFAULT_MODEL_MEMORY_LIMIT[jobType] : DEFAULT_MODEL_MEMORY_LIMIT.outlier_detection; + setEstimatedModelMemoryLimit(fallbackModelMemoryLimit); + setLoadingFieldOptions(false); + setFieldOptionsFetchFail(true); + setMaxDistinctValuesError(maxDistinctValuesErrorMessage); setFormState({ - fieldOptionsFetchFail: true, - maxDistinctValuesError: maxDistinctValuesErrorMessage, - loadingFieldOptions: false, ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: fallbackModelMemoryLimit } : {}), }); } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts new file mode 100644 index 0000000000000..655a5e6a59304 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/fetch_explain_data.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ml } from '../../../../../services/ml_api_service'; +import { extractErrorMessage } from '../../../../../../../common/util/errors'; +import { DfAnalyticsExplainResponse, FieldSelectionItem } from '../../../../common/analytics'; +import { + getJobConfigFromFormState, + State, +} from '../../../analytics_management/hooks/use_create_analytics_form/state'; + +export interface FetchExplainDataReturnType { + success: boolean; + expectedMemory: string; + fieldSelection: FieldSelectionItem[]; + errorMessage: string; +} + +export const fetchExplainData = async (formState: State['form']) => { + const jobConfig = getJobConfigFromFormState(formState); + let errorMessage = ''; + let success = true; + let expectedMemory = ''; + let fieldSelection: FieldSelectionItem[] = []; + + try { + delete jobConfig.dest; + delete jobConfig.model_memory_limit; + const resp: DfAnalyticsExplainResponse = await ml.dataFrameAnalytics.explainDataFrameAnalytics( + jobConfig + ); + expectedMemory = resp.memory_estimation?.expected_memory_without_disk; + fieldSelection = resp.field_selection || []; + } catch (error) { + success = false; + errorMessage = extractErrorMessage(error); + } + + return { + success, + expectedMemory, + fieldSelection, + errorMessage, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/index.ts index ed3f9ef2e9384..45545cf98e0d6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/shared/index.ts @@ -5,3 +5,4 @@ */ export { Messages } from './messages'; +export { fetchExplainData } from './fetch_explain_data'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index 966ef33a1ac8b..ff718277a88a7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -144,7 +144,7 @@ export const Page: FC = ({ jobId }) => { - +

{jobId === undefined && ( { - const { - jobIdEmpty, - jobIdValid, - jobIdExists, - jobType, - createIndexPattern, - excludes, - maxDistinctValuesError, - requiredFieldsError, - } = state.form; + const { jobIdEmpty, jobIdValid, jobIdExists, jobType, createIndexPattern, excludes } = state.form; const { jobConfig } = state; state.advancedEditorMessages = []; @@ -330,8 +321,6 @@ export const validateAdvancedEditor = (state: State): State => { state.form.destinationIndexPatternTitleExists = destinationIndexPatternTitleExists; state.isValid = - maxDistinctValuesError === undefined && - requiredFieldsError === undefined && excludesValid && trainingPercentValid && state.form.modelMemoryLimitUnitValid && @@ -396,10 +385,8 @@ const validateForm = (state: State): State => { destinationIndexPatternTitleExists, createIndexPattern, dependentVariable, - maxDistinctValuesError, modelMemoryLimit, numTopFeatureImportanceValuesValid, - requiredFieldsError, } = state.form; const { estimatedModelMemoryLimit } = state; @@ -414,8 +401,6 @@ const validateForm = (state: State): State => { state.form.modelMemoryLimitValidationResult = mmlValidationResult; state.isValid = - maxDistinctValuesError === undefined && - requiredFieldsError === undefined && !jobTypeEmpty && !mmlValidationResult && !jobIdEmpty && diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 8a07704e39910..241866b56c5c8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiComboBoxOptionOption } from '@elastic/eui'; import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { - FieldSelectionItem, isClassificationAnalysis, isRegressionAnalysis, DataFrameAnalyticsId, @@ -52,8 +50,6 @@ export interface State { computeFeatureInfluence: string; createIndexPattern: boolean; dependentVariable: DependentVariable; - dependentVariableFetchFail: boolean; - dependentVariableOptions: EuiComboBoxOptionOption[]; description: string; destinationIndex: EsIndexName; destinationIndexNameExists: boolean; @@ -62,11 +58,8 @@ export interface State { destinationIndexPatternTitleExists: boolean; eta: undefined | number; excludes: string[]; - excludesTableItems: FieldSelectionItem[]; - excludesOptions: EuiComboBoxOptionOption[]; featureBagFraction: undefined | number; featureInfluenceThreshold: undefined | number; - fieldOptionsFetchFail: boolean; gamma: undefined | number; jobId: DataFrameAnalyticsId; jobIdExists: boolean; @@ -77,9 +70,7 @@ export interface State { jobConfigQuery: any; jobConfigQueryString: string | undefined; lambda: number | undefined; - loadingDepVarOptions: boolean; loadingFieldOptions: boolean; - maxDistinctValuesError: string | undefined; maxTrees: undefined | number; method: undefined | string; modelMemoryLimit: string | undefined; @@ -124,8 +115,6 @@ export const getInitialState = (): State => ({ computeFeatureInfluence: 'true', createIndexPattern: true, dependentVariable: '', - dependentVariableFetchFail: false, - dependentVariableOptions: [], description: '', destinationIndex: '', destinationIndexNameExists: false, @@ -136,10 +125,7 @@ export const getInitialState = (): State => ({ excludes: [], featureBagFraction: undefined, featureInfluenceThreshold: undefined, - fieldOptionsFetchFail: false, gamma: undefined, - excludesTableItems: [], - excludesOptions: [], jobId: '', jobIdExists: false, jobIdEmpty: true, @@ -149,9 +135,7 @@ export const getInitialState = (): State => ({ jobConfigQuery: { match_all: {} }, jobConfigQueryString: undefined, lambda: undefined, - loadingDepVarOptions: false, loadingFieldOptions: false, - maxDistinctValuesError: undefined, maxTrees: undefined, method: undefined, modelMemoryLimit: undefined, @@ -311,6 +295,9 @@ export const getJobConfigFromFormState = ( n_neighbors: formState.nNeighbors, }, formState.outlierFraction && { outlier_fraction: formState.outlierFraction }, + formState.featureInfluenceThreshold && { + feature_influence_threshold: formState.featureInfluenceThreshold, + }, formState.standardizationEnabled && { standardization_enabled: formState.standardizationEnabled, } diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts index 357ea36213521..525e25d0158bf 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts @@ -156,25 +156,45 @@ export default function ({ getService }: FtrProviderContext) { await ml.testResources.deleteIndexPatternByTitle(testData.job.dest!.index as string); }); - it('should open the flyout with a proper header', async () => { - expect(await ml.dataFrameAnalyticsCreation.getHeaderText()).to.be( - `Clone job from ${testData.job.id}` + it('should open the wizard with a proper header', async () => { + expect(await ml.dataFrameAnalyticsCreation.getHeaderText()).to.match( + /Clone analytics job/ ); }); - it('should have correct init form values', async () => { - await ml.dataFrameAnalyticsCreation.assertInitialCloneJobForm( + it('should have correct init form values for config step', async () => { + await ml.dataFrameAnalyticsCreation.assertInitialCloneJobConfigStep( testData.job as DataFrameAnalyticsConfig ); }); - it('should have disabled Create button on open', async () => { - expect(await ml.dataFrameAnalyticsCreation.isCreateButtonDisabled()).to.be(true); + it('should continue to the additional options step', async () => { + await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); }); - it('should enable Create button on a valid form input', async () => { + it('should have correct init form values for additional options step', async () => { + await ml.dataFrameAnalyticsCreation.assertInitialCloneJobAdditionalOptionsStep( + testData.job as DataFrameAnalyticsConfig + ); + }); + + it('should continue to the details step', async () => { + await ml.dataFrameAnalyticsCreation.continueToDetailsStep(); + }); + + it('should have correct init form values for details step', async () => { + await ml.dataFrameAnalyticsCreation.assertInitialCloneJobDetailsStep( + testData.job as DataFrameAnalyticsConfig + ); await ml.dataFrameAnalyticsCreation.setJobId(cloneJobId); await ml.dataFrameAnalyticsCreation.setDestIndex(cloneDestIndex); + }); + + it('should continue to the create step', async () => { + await ml.dataFrameAnalyticsCreation.continueToCreateStep(); + }); + + it('should have enabled Create button on a valid form input', async () => { expect(await ml.dataFrameAnalyticsCreation.isCreateButtonDisabled()).to.be(false); }); @@ -182,11 +202,12 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.createAnalyticsJob(cloneJobId); }); - it('finishes analytics processing', async () => { + it('should finish analytics processing', async () => { await ml.dataFrameAnalytics.waitForAnalyticsCompletion(cloneJobId); }); - it('displays the created job in the analytics table', async () => { + it('should display the created job in the analytics table', async () => { + await ml.dataFrameAnalyticsCreation.navigateToJobManagementPage(); await ml.dataFrameAnalyticsTable.refreshAnalyticsTable(); await ml.dataFrameAnalyticsTable.filterWithSearchString(cloneJobId); const rows = await ml.dataFrameAnalyticsTable.parseAnalyticsTable(); diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index 081eb8775fa5b..f67ea583e25cd 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -124,37 +124,15 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( await this.assertJobDescriptionValue(jobDescription); }, - async assertSourceIndexInputExists() { - await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutSourceIndexSelect > comboBoxInput'); - }, - - async assertSourceIndexSelection(expectedSelection: string[]) { - const actualSelection = await comboBox.getComboBoxSelectedOptions( - 'mlAnalyticsCreateJobFlyoutSourceIndexSelect > comboBoxInput' - ); - expect(actualSelection).to.eql( - expectedSelection, - `Source index should be '${expectedSelection}' (got '${actualSelection}')` - ); - }, - - async assertExcludedFieldsSelection(expectedSelection: string[]) { - const actualSelection = await comboBox.getComboBoxSelectedOptions( - 'mlAnalyticsCreateJobFlyoutExcludesSelect > comboBoxInput' - ); - expect(actualSelection).to.eql( - expectedSelection, - `Excluded fields should be '${expectedSelection}' (got '${actualSelection}')` - ); - }, - - async selectSourceIndex(sourceIndex: string) { - await comboBox.set( - 'mlAnalyticsCreateJobFlyoutSourceIndexSelect > comboBoxInput', - sourceIndex - ); - await this.assertSourceIndexSelection([sourceIndex]); - }, + // async assertExcludedFieldsSelection(expectedSelection: string[]) { + // const actualSelection = await comboBox.getComboBoxSelectedOptions( + // 'mlAnalyticsCreateJobWizardExcludesSelect' + // ); + // expect(actualSelection).to.eql( + // expectedSelection, + // `Excluded fields should be '${expectedSelection}' (got '${actualSelection}')` + // ); + // }, async assertDestIndexInputExists() { await testSubjects.existOrFail('mlAnalyticsCreateJobFlyoutDestinationIndexInput'); @@ -384,24 +362,29 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( }, async getHeaderText() { - return await testSubjects.getVisibleText('mlDataFrameAnalyticsFlyoutHeaderTitle'); + return await testSubjects.getVisibleText('mlDataFrameAnalyticsWizardHeaderTitle'); }, - async assertInitialCloneJobForm(job: DataFrameAnalyticsConfig) { + async assertInitialCloneJobConfigStep(job: DataFrameAnalyticsConfig) { const jobType = Object.keys(job.analysis)[0]; await this.assertJobTypeSelection(jobType); - await this.assertJobIdValue(''); // id should be empty - await this.assertJobDescriptionValue(String(job.description)); - await this.assertSourceIndexSelection(job.source.index as string[]); - await this.assertDestIndexValue(''); // destination index should be empty if (isClassificationAnalysis(job.analysis) || isRegressionAnalysis(job.analysis)) { await this.assertDependentVariableSelection([job.analysis[jobType].dependent_variable]); await this.assertTrainingPercentValue(String(job.analysis[jobType].training_percent)); } - await this.assertExcludedFieldsSelection(job.analyzed_fields.excludes); + // await this.assertExcludedFieldsSelection(job.analyzed_fields.excludes); + }, + + async assertInitialCloneJobAdditionalOptionsStep(job: DataFrameAnalyticsConfig) { await this.assertModelMemoryValue(job.model_memory_limit); }, + async assertInitialCloneJobDetailsStep(job: DataFrameAnalyticsConfig) { + await this.assertJobIdValue(''); // id should be empty + await this.assertJobDescriptionValue(String(job.description)); + await this.assertDestIndexValue(''); // destination index should be empty + }, + async assertCreationCalloutMessagesExist() { await testSubjects.existOrFail('analyticsWizardCreationCallout_0'); await testSubjects.existOrFail('analyticsWizardCreationCallout_1'); diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts index 60507f5ab3331..f452c9cce7a1a 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts @@ -126,7 +126,7 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F public async cloneJob(analyticsId: string) { await this.openRowActions(analyticsId); await testSubjects.click(`mlAnalyticsJobCloneButton`); - await testSubjects.existOrFail('mlAnalyticsCreateJobFlyout'); + await testSubjects.existOrFail('mlAnalyticsCreationContainer'); } })(); } From 5f7de497b227b5bc49f137a1801f3e3c753eba62 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 25 Jun 2020 16:55:22 +0200 Subject: [PATCH 33/82] [7.x] [APM] Use asPercent to format breakdown chart (#69384) (#69886) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Søren Louv-Jansen Co-authored-by: Elastic Machine Co-authored-by: Søren Louv-Jansen Co-authored-by: Elastic Machine --- .../TransactionBreakdownGraph/index.tsx | 3 +-- .../TransactionBreakdownKpiList.tsx | 6 ++---- .../utils/formatters/__test__/formatters.test.ts | 10 +++++++--- .../plugins/apm/public/utils/formatters/formatters.ts | 8 ++++++++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx index 0afed6c3c1fa8..2cb3696f88002 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -5,7 +5,6 @@ */ import React, { useMemo } from 'react'; -import numeral from '@elastic/numeral'; import { throttle } from 'lodash'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; @@ -21,7 +20,7 @@ interface Props { } const tickFormatY = (y: Maybe) => { - return numeral(y || 0).format('0 %'); + return asPercent(y ?? 0, 1); }; const formatTooltipValue = (coordinate: Coordinate) => { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx index eda8f19c949a3..3898679f83537 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx @@ -13,7 +13,7 @@ import { EuiIcon, } from '@elastic/eui'; import styled from 'styled-components'; -import { FORMATTERS, InfraFormatterType } from '../../../../../infra/public'; +import { asPercent } from '../../../utils/formatters'; interface TransactionBreakdownKpi { name: string; @@ -65,9 +65,7 @@ const TransactionBreakdownKpiList: React.FC = ({ kpis }) => { - - {FORMATTERS[InfraFormatterType.percent](kpi.percentage)} - + {asPercent(kpi.percentage, 1)} diff --git a/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts index f6ed88a850a5b..66101baf3a746 100644 --- a/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts +++ b/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts @@ -7,12 +7,16 @@ 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 as integer when number is above 10', () => { + expect(asPercent(3725, 10000, 'n/a')).toEqual('37%'); + }); + + it('should add a decimal when value is below 10', () => { + expect(asPercent(0.092, 1)).toEqual('9.2%'); }); it('should format when numerator is 0', () => { - expect(asPercent(0, 1, 'n/a')).toEqual('0.0%'); + expect(asPercent(0, 1, 'n/a')).toEqual('0%'); }); it('should return fallback when denominator is undefined', () => { diff --git a/x-pack/plugins/apm/public/utils/formatters/formatters.ts b/x-pack/plugins/apm/public/utils/formatters/formatters.ts index 9fdac85c7154f..649f11063b149 100644 --- a/x-pack/plugins/apm/public/utils/formatters/formatters.ts +++ b/x-pack/plugins/apm/public/utils/formatters/formatters.ts @@ -34,5 +34,13 @@ export function asPercent( } const decimal = numerator / denominator; + + // 33.2 => 33% + // 3.32 => 3.3% + // 0 => 0% + if (Math.abs(decimal) >= 0.1 || decimal === 0) { + return numeral(decimal).format('0%'); + } + return numeral(decimal).format('0.0%'); } From 1857c94c8ba8dae1282e2afa74e67ff0306157fd Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 25 Jun 2020 16:55:29 +0200 Subject: [PATCH 34/82] [7.x] [APM] Script for metric aggregation (#67964) (#69887) * [APM] Script for metric aggregation * Retry mechanism * Docs/comments * compress histogram; support --filter & --only parameters * Add flag for significant figures * Ignore apm scripts * Add tsconfig project for apm/scripts --- src/dev/typescript/projects.ts | 4 + .../apm/scripts/aggregate-latency-metrics.js | 31 ++ .../aggregate-latency-metrics/index.ts | 444 ++++++++++++++++++ x-pack/plugins/apm/scripts/package.json | 5 +- .../scripts/shared/create-or-update-index.ts | 60 +++ .../download-telemetry-template.ts | 19 +- .../apm/scripts/shared/get-http-auth.ts | 19 + .../apm/scripts/shared/read-kibana-config.ts | 49 ++ .../apm/scripts/shared/stamp-logger.ts | 11 + x-pack/plugins/apm/scripts/tsconfig.json | 12 + .../scripts/upload-telemetry-data/index.ts | 164 +++---- x-pack/tsconfig.json | 1 + 12 files changed, 704 insertions(+), 115 deletions(-) create mode 100644 x-pack/plugins/apm/scripts/aggregate-latency-metrics.js create mode 100644 x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts create mode 100644 x-pack/plugins/apm/scripts/shared/create-or-update-index.ts rename x-pack/plugins/apm/scripts/{upload-telemetry-data => shared}/download-telemetry-template.ts (68%) create mode 100644 x-pack/plugins/apm/scripts/shared/get-http-auth.ts create mode 100644 x-pack/plugins/apm/scripts/shared/read-kibana-config.ts create mode 100644 x-pack/plugins/apm/scripts/shared/stamp-logger.ts create mode 100644 x-pack/plugins/apm/scripts/tsconfig.json diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 1e0b631308d9e..065321e355256 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -34,6 +34,10 @@ export const PROJECTS = [ name: 'apm/cypress', disableTypeCheck: true, }), + new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/scripts/tsconfig.json'), { + name: 'apm/scripts', + disableTypeCheck: true, + }), // NOTE: using glob.sync rather than glob-all or globby // because it takes less than 10 ms, while the other modules diff --git a/x-pack/plugins/apm/scripts/aggregate-latency-metrics.js b/x-pack/plugins/apm/scripts/aggregate-latency-metrics.js new file mode 100644 index 0000000000000..287f267343b11 --- /dev/null +++ b/x-pack/plugins/apm/scripts/aggregate-latency-metrics.js @@ -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. + */ +// eslint-disable-next-line import/no-extraneous-dependencies +require('@babel/register')({ + extensions: ['.ts'], + plugins: [ + '@babel/plugin-proposal-optional-chaining', + '@babel/plugin-proposal-nullish-coalescing-operator', + ], + presets: [ + '@babel/typescript', + ['@babel/preset-env', { targets: { node: 'current' } }], + ], +}); + +const { + aggregateLatencyMetrics, +} = require('./aggregate-latency-metrics/index.ts'); + +aggregateLatencyMetrics().catch((err) => { + if (err.meta && err.meta.body) { + // error from elasticsearch client + console.error(err.meta.body); + } else { + console.error(err); + } + process.exit(1); +}); diff --git a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts new file mode 100644 index 0000000000000..6bc370be903df --- /dev/null +++ b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts @@ -0,0 +1,444 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Client } from '@elastic/elasticsearch'; +import { argv } from 'yargs'; +import pLimit from 'p-limit'; +import pRetry from 'p-retry'; +import { parse, format } from 'url'; +import { unique, without, set, merge, flatten } from 'lodash'; +import * as histogram from 'hdr-histogram-js'; +import { ESSearchResponse } from '../../typings/elasticsearch'; +import { + HOST_NAME, + SERVICE_NAME, + TRANSACTION_NAME, + TRANSACTION_TYPE, + AGENT_NAME, + SERVICE_ENVIRONMENT, + POD_NAME, + CONTAINER_ID, + SERVICE_VERSION, + TRANSACTION_RESULT, + PROCESSOR_EVENT, +} from '../../common/elasticsearch_fieldnames'; +import { stampLogger } from '../shared/stamp-logger'; +import { createOrUpdateIndex } from '../shared/create-or-update-index'; + +// This script will try to estimate how many latency metric documents +// will be created based on the available transaction documents. +// It can also generate metric documents based on a painless script +// and hdr histograms. +// +// Options: +// - interval: the interval (in minutes) for which latency metrics will be aggregated. +// Defaults to 1. +// - concurrency: number of maximum concurrent requests to ES. Defaults to 3. +// - from: start of the date range that should be processed. Should be a valid ISO timestamp. +// - to: end of the date range that should be processed. Should be a valid ISO timestamp. +// - source: from which transaction documents should be read. Should be location of ES (basic auth +// is supported) plus the index name (or an index pattern). Example: +// https://foo:bar@apm.elstc.co:9999/apm-8.0.0-transaction +// - dest: to which metric documents should be written. If this is not set, no metric documents +// will be created.Should be location of ES (basic auth is supported) plus the index name. +// Example: https://foo:bar@apm.elstc.co:9999/apm-8.0.0-metric +// - include: comma-separated list of fields that should be aggregated on, in addition to the +// default ones. +// - exclude: comma-separated list of fields that should be not be aggregated on. + +stampLogger(); + +export async function aggregateLatencyMetrics() { + const interval = parseInt(String(argv.interval), 10) || 1; + const concurrency = parseInt(String(argv.concurrency), 10) || 3; + const numSigFigures = (parseInt(String(argv.sigfig), 10) || 2) as + | 1 + | 2 + | 3 + | 4 + | 5; + + const from = new Date(String(argv.from)).getTime(); + const to = new Date(String(argv.to)).getTime(); + + if (isNaN(from) || isNaN(to)) { + throw new Error( + `from and to are not valid dates - please supply valid ISO timestamps` + ); + } + + if (to <= from) { + throw new Error('to cannot be earlier than from'); + } + + const limit = pLimit(concurrency); + // retry function to handle ES timeouts + const retry = (fn: (...args: any[]) => any) => { + return () => + pRetry(fn, { + factor: 1, + retries: 3, + minTimeout: 2500, + }); + }; + + const tasks: Array> = []; + + const defaultFields = [ + SERVICE_NAME, + SERVICE_VERSION, + SERVICE_ENVIRONMENT, + AGENT_NAME, + HOST_NAME, + POD_NAME, + CONTAINER_ID, + TRANSACTION_NAME, + TRANSACTION_RESULT, + TRANSACTION_TYPE, + ]; + + const include = String(argv.include ?? '') + .split(',') + .filter(Boolean) as string[]; + + const exclude = String(argv.exclude ?? '') + .split(',') + .filter(Boolean) as string[]; + + const only = String(argv.only ?? '') + .split(',') + .filter(Boolean) as string[]; + + const fields = only.length + ? unique(only) + : without(unique([...include, ...defaultFields]), ...exclude); + + const globalFilter = argv.filter ? JSON.parse(String(argv.filter)) : {}; + + // eslint-disable-next-line no-console + console.log('Aggregating on', fields.join(',')); + + const source = String(argv.source ?? ''); + const dest = String(argv.dest ?? ''); + + function getClientOptionsFromIndexUrl( + url: string + ): { node: string; index: string } { + const parsed = parse(url); + const { pathname, ...rest } = parsed; + + return { + node: format(rest), + index: pathname!.replace('/', ''), + }; + } + + const sourceOptions = getClientOptionsFromIndexUrl(source); + + const sourceClient = new Client({ + node: sourceOptions.node, + ssl: { + rejectUnauthorized: false, + }, + requestTimeout: 120000, + }); + + let destClient: Client | undefined; + let destOptions: { node: string; index: string } | undefined; + + const uploadMetrics = !!dest; + + if (uploadMetrics) { + destOptions = getClientOptionsFromIndexUrl(dest); + destClient = new Client({ + node: destOptions.node, + ssl: { + rejectUnauthorized: false, + }, + }); + + const mappings = ( + await sourceClient.indices.getMapping({ + index: sourceOptions.index, + }) + ).body; + + const lastMapping = mappings[Object.keys(mappings)[0]]; + + const newMapping = merge({}, lastMapping, { + mappings: { + properties: { + transaction: { + properties: { + duration: { + properties: { + histogram: { + type: 'histogram', + }, + }, + }, + }, + }, + }, + }, + }); + + await createOrUpdateIndex({ + client: destClient, + indexName: destOptions.index, + clear: false, + template: newMapping, + }); + } else { + // eslint-disable-next-line no-console + console.log( + 'No destination was defined, not uploading aggregated documents' + ); + } + + let at = to; + while (at > from) { + const end = at; + const start = Math.max(from, at - interval * 60 * 1000); + + tasks.push( + limit( + retry(async () => { + const filter = [ + { + term: { + [PROCESSOR_EVENT]: 'transaction', + }, + }, + { + range: { + '@timestamp': { + gte: start, + lt: end, + }, + }, + }, + ]; + + const query: { + query: Record; + } = { + ...globalFilter, + query: { + ...(globalFilter?.query ?? {}), + bool: { + ...(globalFilter?.query?.bool ?? {}), + filter: [ + ...Object.values(globalFilter?.query?.bool?.filter ?? {}), + ...filter, + ], + }, + }, + }; + + async function paginateThroughBuckets( + buckets: Array<{ + doc_count: number; + key: any; + recorded_values?: { value: unknown }; + }>, + after?: any + ): Promise< + Array<{ + doc_count: number; + key: any; + recorded_values?: { value: unknown }; + }> + > { + const params = { + index: sourceOptions.index, + body: { + ...query, + aggs: { + transactionGroups: { + composite: { + ...(after ? { after } : {}), + size: 10000, + sources: fields.map((field) => ({ + [field]: { + terms: { + field, + missing_bucket: true, + }, + }, + })), + }, + ...(dest + ? { + // scripted metric agg to get all the values (rather than downloading all the documents) + aggs: { + recorded_values: { + scripted_metric: { + init_script: 'state.values = new ArrayList()', + map_script: ` + if (!doc['transaction.duration.us'].empty) { + state.values.add(doc['transaction.duration.us'].value); + } + `, + combine_script: 'return state.values', + reduce_script: ` + return states.stream().flatMap(l -> l.stream()).collect(Collectors.toList()) + `, + }, + }, + }, + } + : {}), + }, + }, + }, + }; + + const response = (await sourceClient.search(params)) + .body as ESSearchResponse; + + const { aggregations } = response; + + if (!aggregations) { + return buckets; + } + + const { transactionGroups } = aggregations; + + const nextBuckets = buckets.concat(transactionGroups.buckets); + + if (!transactionGroups.after_key) { + return nextBuckets; + } + + return nextBuckets.concat( + await paginateThroughBuckets(buckets, transactionGroups.after_key) + ); + } + + async function getNumberOfTransactionDocuments() { + const params = { + index: sourceOptions.index, + body: { + query: { + bool: { + filter, + }, + }, + track_total_hits: true, + }, + }; + + const response = (await sourceClient.search(params)) + .body as ESSearchResponse; + + return response.hits.total.value; + } + + const [buckets, numberOfTransactionDocuments] = await Promise.all([ + paginateThroughBuckets([]), + getNumberOfTransactionDocuments(), + ]); + + const rangeLabel = `${new Date(start).toISOString()}-${new Date( + end + ).toISOString()}`; + + // eslint-disable-next-line no-console + console.log( + `${rangeLabel}: Compression: ${ + buckets.length + }/${numberOfTransactionDocuments} (${( + (buckets.length / numberOfTransactionDocuments) * + 100 + ).toPrecision(2)}%)` + ); + + const docs: Array> = []; + + if (uploadMetrics) { + buckets.forEach((bucket) => { + const values = (bucket.recorded_values?.value ?? []) as number[]; + const h = histogram.build({ + numberOfSignificantValueDigits: numSigFigures, + }); + values.forEach((value) => { + h.recordValue(value); + }); + + const iterator = h.recordedValuesIterator; + + const distribution = { + values: [] as number[], + counts: [] as number[], + }; + + iterator.reset(); + + while (iterator.hasNext()) { + const value = iterator.next(); + distribution.values.push(value.valueIteratedTo); + distribution.counts.push(value.countAtValueIteratedTo); + } + + const structured = Object.keys(bucket.key).reduce((prev, key) => { + set(prev, key, bucket.key[key]); + return prev; + }, {}); + + const doc = merge({}, structured, { + '@timestamp': new Date(start).toISOString(), + timestamp: { + us: start * 1000, + }, + processor: { + name: 'metric', + event: 'metric', + }, + transaction: { + duration: { + histogram: distribution, + }, + }, + }); + + docs.push(doc); + }); + + if (!docs.length) { + // eslint-disable-next-line no-console + console.log(`${rangeLabel}: No docs to upload`); + return; + } + + const response = await destClient?.bulk({ + refresh: 'wait_for', + body: flatten( + docs.map((doc) => [ + { index: { _index: destOptions?.index } }, + doc, + ]) + ), + }); + + if (response?.body.errors) { + throw new Error( + `${rangeLabel}: Could not upload all metric documents` + ); + } + // eslint-disable-next-line no-console + console.log( + `${rangeLabel}: Uploaded ${docs.length} metric documents` + ); + } + }) + ) + ); + at = start; + } + + await Promise.all(tasks); +} diff --git a/x-pack/plugins/apm/scripts/package.json b/x-pack/plugins/apm/scripts/package.json index 9121449c53619..c5a9df792f856 100644 --- a/x-pack/plugins/apm/scripts/package.json +++ b/x-pack/plugins/apm/scripts/package.json @@ -4,7 +4,10 @@ "main": "index.js", "license": "MIT", "dependencies": { + "@elastic/elasticsearch": "^7.6.1", "@octokit/rest": "^16.35.0", - "console-stamp": "^0.2.9" + "@types/console-stamp": "^0.2.32", + "console-stamp": "^0.2.9", + "hdr-histogram-js": "^1.2.0" } } diff --git a/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts b/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts new file mode 100644 index 0000000000000..3f88b73f55984 --- /dev/null +++ b/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Client } from '@elastic/elasticsearch'; + +export async function createOrUpdateIndex({ + client, + clear, + indexName, + template, +}: { + client: Client; + clear: boolean; + indexName: string; + template: any; +}) { + if (clear) { + try { + await client.indices.delete({ + index: indexName, + }); + } catch (err) { + // 404 = index not found, totally okay + if (err.body.status !== 404) { + throw err; + } + } + } + + const indexExists = ( + await client.indices.exists({ + index: indexName, + }) + ).body as boolean; + + if (!indexExists) { + await client.indices.create({ + index: indexName, + body: template, + }); + } else { + await Promise.all([ + template.mappings + ? client.indices.putMapping({ + index: indexName, + body: template.mappings, + }) + : Promise.resolve(undefined as any), + template.settings + ? client.indices.putSettings({ + index: indexName, + body: template.settings, + }) + : Promise.resolve(undefined as any), + ]); + } +} diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/download-telemetry-template.ts b/x-pack/plugins/apm/scripts/shared/download-telemetry-template.ts similarity index 68% rename from x-pack/plugins/apm/scripts/upload-telemetry-data/download-telemetry-template.ts rename to x-pack/plugins/apm/scripts/shared/download-telemetry-template.ts index 31559f1ab3c78..f20c6328281f4 100644 --- a/x-pack/plugins/apm/scripts/upload-telemetry-data/download-telemetry-template.ts +++ b/x-pack/plugins/apm/scripts/shared/download-telemetry-template.ts @@ -4,15 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore import { Octokit } from '@octokit/rest'; -export async function downloadTelemetryTemplate(octokit: Octokit) { +export async function downloadTelemetryTemplate({ + githubToken, +}: { + githubToken: string; +}) { + const octokit = new Octokit({ + auth: githubToken, + }); const file = await octokit.repos.getContents({ owner: 'elastic', repo: 'telemetry', path: 'config/templates/xpack-phone-home.json', - // @ts-ignore mediaType: { format: 'application/vnd.github.VERSION.raw', }, @@ -22,5 +27,11 @@ export async function downloadTelemetryTemplate(octokit: Octokit) { throw new Error('Expected single response, got array'); } - return JSON.parse(Buffer.from(file.data.content!, 'base64').toString()); + return JSON.parse(Buffer.from(file.data.content!, 'base64').toString()) as { + index_patterns: string[]; + mappings: { + properties: Record; + }; + settings: Record; + }; } diff --git a/x-pack/plugins/apm/scripts/shared/get-http-auth.ts b/x-pack/plugins/apm/scripts/shared/get-http-auth.ts new file mode 100644 index 0000000000000..b662deb863a35 --- /dev/null +++ b/x-pack/plugins/apm/scripts/shared/get-http-auth.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaConfig } from './read-kibana-config'; + +export const getHttpAuth = (config: KibanaConfig) => { + const httpAuth = + config['elasticsearch.username'] && config['elasticsearch.password'] + ? { + username: config['elasticsearch.username'], + password: config['elasticsearch.password'], + } + : null; + + return httpAuth; +}; diff --git a/x-pack/plugins/apm/scripts/shared/read-kibana-config.ts b/x-pack/plugins/apm/scripts/shared/read-kibana-config.ts new file mode 100644 index 0000000000000..bc5f1afc63cac --- /dev/null +++ b/x-pack/plugins/apm/scripts/shared/read-kibana-config.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import path from 'path'; +import fs from 'fs'; +import yaml from 'js-yaml'; +import { identity, pick } from 'lodash'; + +export type KibanaConfig = ReturnType; + +export const readKibanaConfig = () => { + const kibanaConfigDir = path.join(__filename, '../../../../../../config'); + const kibanaDevConfig = path.join(kibanaConfigDir, 'kibana.dev.yml'); + const kibanaConfig = path.join(kibanaConfigDir, 'kibana.yml'); + + const loadedKibanaConfig = (yaml.safeLoad( + fs.readFileSync( + fs.existsSync(kibanaDevConfig) ? kibanaDevConfig : kibanaConfig, + 'utf8' + ) + ) || {}) as {}; + + const cliEsCredentials = pick( + { + 'elasticsearch.username': process.env.ELASTICSEARCH_USERNAME, + 'elasticsearch.password': process.env.ELASTICSEARCH_PASSWORD, + 'elasticsearch.hosts': process.env.ELASTICSEARCH_HOST, + }, + identity + ) as { + 'elasticsearch.username'?: string; + 'elasticsearch.password'?: string; + 'elasticsearch.hosts'?: string; + }; + + return { + 'apm_oss.transactionIndices': 'apm-*', + 'apm_oss.metricsIndices': 'apm-*', + 'apm_oss.errorIndices': 'apm-*', + 'apm_oss.spanIndices': 'apm-*', + 'apm_oss.onboardingIndices': 'apm-*', + 'apm_oss.sourcemapIndices': 'apm-*', + 'elasticsearch.hosts': 'http://localhost:9200', + ...loadedKibanaConfig, + ...cliEsCredentials, + }; +}; diff --git a/x-pack/plugins/apm/scripts/shared/stamp-logger.ts b/x-pack/plugins/apm/scripts/shared/stamp-logger.ts new file mode 100644 index 0000000000000..65d24bbae7008 --- /dev/null +++ b/x-pack/plugins/apm/scripts/shared/stamp-logger.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import consoleStamp from 'console-stamp'; + +export function stampLogger() { + consoleStamp(console, { pattern: '[HH:MM:ss.l]' }); +} diff --git a/x-pack/plugins/apm/scripts/tsconfig.json b/x-pack/plugins/apm/scripts/tsconfig.json new file mode 100644 index 0000000000000..350db55e72446 --- /dev/null +++ b/x-pack/plugins/apm/scripts/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "include": [ + "./**/*" + ], + "exclude": [], + "compilerOptions": { + "types": [ + "node" + ] + } +} diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts index a3c97cd8828d8..5f9c72810fc91 100644 --- a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts +++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts @@ -11,115 +11,50 @@ // - Easier testing of the telemetry tasks // - Validate whether we can run the queries we want to on the telemetry data -import fs from 'fs'; -import path from 'path'; -// @ts-ignore -import { Octokit } from '@octokit/rest'; -import { merge, chunk, flatten, pick, identity } from 'lodash'; -import axios from 'axios'; -import yaml from 'js-yaml'; -import { Client } from 'elasticsearch'; +import { merge, chunk, flatten } from 'lodash'; +import { Client } from '@elastic/elasticsearch'; import { argv } from 'yargs'; -import { promisify } from 'util'; import { Logger } from 'kibana/server'; -// @ts-ignore -import consoleStamp from 'console-stamp'; +import { stampLogger } from '../shared/stamp-logger'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { CollectTelemetryParams } from '../../server/lib/apm_telemetry/collect_data_telemetry'; -import { downloadTelemetryTemplate } from './download-telemetry-template'; -import mapping from '../../mappings.json'; +import { downloadTelemetryTemplate } from '../shared/download-telemetry-template'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { apmTelemetry } from '../../server/saved_objects/apm_telemetry'; import { generateSampleDocuments } from './generate-sample-documents'; +import { readKibanaConfig } from '../shared/read-kibana-config'; +import { getHttpAuth } from '../shared/get-http-auth'; +import { createOrUpdateIndex } from '../shared/create-or-update-index'; -consoleStamp(console, '[HH:MM:ss.l]'); - -const githubToken = process.env.GITHUB_TOKEN; +stampLogger(); -if (!githubToken) { - throw new Error('GITHUB_TOKEN was not provided.'); -} +async function uploadData() { + const githubToken = process.env.GITHUB_TOKEN; -const kibanaConfigDir = path.join(__filename, '../../../../../../config'); -const kibanaDevConfig = path.join(kibanaConfigDir, 'kibana.dev.yml'); -const kibanaConfig = path.join(kibanaConfigDir, 'kibana.yml'); - -const xpackTelemetryIndexName = 'xpack-phone-home'; - -const loadedKibanaConfig = (yaml.safeLoad( - fs.readFileSync( - fs.existsSync(kibanaDevConfig) ? kibanaDevConfig : kibanaConfig, - 'utf8' - ) -) || {}) as {}; - -const cliEsCredentials = pick( - { - 'elasticsearch.username': process.env.ELASTICSEARCH_USERNAME, - 'elasticsearch.password': process.env.ELASTICSEARCH_PASSWORD, - 'elasticsearch.hosts': process.env.ELASTICSEARCH_HOST, - }, - identity -) as { - 'elasticsearch.username'?: string; - 'elasticsearch.password'?: string; - 'elasticsearch.hosts'?: string; -}; - -const config = { - 'apm_oss.transactionIndices': 'apm-*', - 'apm_oss.metricsIndices': 'apm-*', - 'apm_oss.errorIndices': 'apm-*', - 'apm_oss.spanIndices': 'apm-*', - 'apm_oss.onboardingIndices': 'apm-*', - 'apm_oss.sourcemapIndices': 'apm-*', - 'elasticsearch.hosts': 'http://localhost:9200', - ...loadedKibanaConfig, - ...cliEsCredentials, -}; + if (!githubToken) { + throw new Error('GITHUB_TOKEN was not provided.'); + } -async function uploadData() { - const octokit = new Octokit({ - auth: githubToken, + const xpackTelemetryIndexName = 'xpack-phone-home'; + const telemetryTemplate = await downloadTelemetryTemplate({ + githubToken, }); - const telemetryTemplate = await downloadTelemetryTemplate(octokit); + const kibanaMapping = apmTelemetry.mappings; - const kibanaMapping = mapping['apm-telemetry']; + const config = readKibanaConfig(); - const httpAuth = - config['elasticsearch.username'] && config['elasticsearch.password'] - ? { - username: config['elasticsearch.username'], - password: config['elasticsearch.password'], - } - : null; + const httpAuth = getHttpAuth(config); const client = new Client({ - host: config['elasticsearch.hosts'], + nodes: [config['elasticsearch.hosts']], ...(httpAuth ? { - httpAuth: `${httpAuth.username}:${httpAuth.password}`, + auth: httpAuth, } : {}), }); - if (argv.clear) { - try { - await promisify(client.indices.delete.bind(client))({ - index: xpackTelemetryIndexName, - }); - } catch (err) { - // 404 = index not found, totally okay - if (err.status !== 404) { - throw err; - } - } - } - - const axiosInstance = axios.create({ - baseURL: config['elasticsearch.hosts'], - ...(httpAuth ? { auth: httpAuth } : {}), - }); - const newTemplate = merge(telemetryTemplate, { settings: { index: { mapping: { total_fields: { limit: 10000 } } }, @@ -129,7 +64,12 @@ async function uploadData() { // override apm mapping instead of merging newTemplate.mappings.properties.stack_stats.properties.kibana.properties.plugins.properties.apm = kibanaMapping; - await axiosInstance.put(`/_template/xpack-phone-home`, newTemplate); + await createOrUpdateIndex({ + indexName: xpackTelemetryIndexName, + client, + template: newTemplate, + clear: !!argv.clear, + }); const sampleDocuments = await generateSampleDocuments({ collectTelemetryParams: { @@ -140,19 +80,16 @@ async function uploadData() { apmAgentConfigurationIndex: '.apm-agent-configuration', }, search: (body) => { - return promisify(client.search.bind(client))({ - ...body, - requestTimeout: 120000, - }) as any; + return client.search(body as any).then((res) => res.body); }, indicesStats: (body) => { - return promisify(client.indices.stats.bind(client))({ - ...body, - requestTimeout: 120000, - }) as any; + return client.indices.stats(body as any); }, transportRequest: ((params) => { - return axiosInstance[params.method](params.path); + return client.transport.request({ + method: params.method, + path: params.path, + }); }) as CollectTelemetryParams['transportRequest'], }, }); @@ -162,20 +99,27 @@ async function uploadData() { await chunks.reduce>((prev, documents) => { return prev.then(async () => { const body = flatten( - documents.map((doc) => [{ index: { _index: 'xpack-phone-home' } }, doc]) + documents.map((doc) => [ + { index: { _index: xpackTelemetryIndexName } }, + doc, + ]) ); - return promisify(client.bulk.bind(client))({ - body, - refresh: true, - }).then((response: any) => { - if (response.errors) { - const firstError = response.items.filter( - (item: any) => item.index.status >= 400 - )[0].index.error; - throw new Error(`Failed to upload documents: ${firstError.reason} `); - } - }); + return client + .bulk({ + body, + refresh: 'wait_for', + }) + .then((response: any) => { + if (response.errors) { + const firstError = response.items.filter( + (item: any) => item.index.status >= 400 + )[0].index.error; + throw new Error( + `Failed to upload documents: ${firstError.reason} ` + ); + } + }); }); }, Promise.resolve()); } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 306294c57b3c6..e978702a35634 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -14,6 +14,7 @@ "test/**/*", "plugins/security_solution/cypress/**/*", "plugins/apm/e2e/cypress/**/*", + "plugins/apm/scripts/**/*", "**/typespec_tests.ts" ], "compilerOptions": { From 4f386e1bab76d99ce82c5f94448f7a01df03b30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 25 Jun 2020 16:59:02 +0200 Subject: [PATCH 35/82] [7.x] [Logs UI] Access ML via the programmatic plugin API (#68905) (#69921) Backports the following commits to 7.x: - [Logs UI] Access ML via the programmatic plugin API (#68905) --- x-pack/plugins/infra/kibana.json | 3 + .../lib/adapters/framework/adapter_types.ts | 13 +- .../infra/server/lib/compose/kibana.ts | 59 -- .../plugins/infra/server/lib/infra_types.ts | 3 - .../log_entry_categories_analysis.ts | 862 +++++++++--------- .../log_analysis/log_entry_rate_analysis.ts | 208 ++--- .../server/lib/log_analysis/queries/common.ts | 22 +- .../queries/log_entry_categories.ts | 9 +- .../queries/log_entry_category_histograms.ts | 7 +- .../queries/log_entry_data_sets.ts | 27 +- .../log_analysis/queries/log_entry_rate.ts | 25 +- .../queries/top_log_entry_categories.ts | 9 +- x-pack/plugins/infra/server/plugin.ts | 27 +- .../results/log_entry_categories.ts | 60 +- .../results/log_entry_category_datasets.ts | 58 +- .../results/log_entry_category_examples.ts | 58 +- .../log_analysis/results/log_entry_rate.ts | 61 +- x-pack/plugins/infra/server/types.ts | 28 + .../infra/server/utils/request_context.ts | 43 + .../apis/metrics_ui/log_analysis.ts | 25 +- 20 files changed, 773 insertions(+), 834 deletions(-) delete mode 100644 x-pack/plugins/infra/server/lib/compose/kibana.ts create mode 100644 x-pack/plugins/infra/server/types.ts create mode 100644 x-pack/plugins/infra/server/utils/request_context.ts diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index 4701182c96813..4e23f1985d450 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -13,6 +13,9 @@ "alerts", "triggers_actions_ui" ], + "optionalPlugins": [ + "ml" + ], "server": true, "ui": true, "configPath": ["xpack", "infra"] diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index eca03c6b9643f..a61d237aa63c6 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -4,18 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse, GenericParams } from 'elasticsearch'; +import { GenericParams, SearchResponse } from 'elasticsearch'; import { Lifecycle } from 'hapi'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { RouteMethod, RouteConfig } from '../../../../../../../src/core/server'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../plugins/features/server'; -import { SpacesPluginSetup } from '../../../../../../plugins/spaces/server'; +import { RouteConfig, RouteMethod } from '../../../../../../../src/core/server'; +import { HomeServerPluginSetup } from '../../../../../../../src/plugins/home/server'; import { VisTypeTimeseriesSetup } from '../../../../../../../src/plugins/vis_type_timeseries/server'; import { APMPluginSetup } from '../../../../../../plugins/apm/server'; -import { HomeServerPluginSetup } from '../../../../../../../src/plugins/home/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../plugins/features/server'; +import { SpacesPluginSetup } from '../../../../../../plugins/spaces/server'; import { PluginSetupContract as AlertingPluginContract } from '../../../../../alerts/server'; +import { MlPluginSetup } from '../../../../../ml/server'; -// NP_TODO: Compose real types from plugins we depend on, no "any" export interface InfraServerPluginDeps { home: HomeServerPluginSetup; spaces: SpacesPluginSetup; @@ -24,6 +24,7 @@ export interface InfraServerPluginDeps { features: FeaturesPluginSetup; apm: APMPluginSetup; alerts: AlertingPluginContract; + ml?: MlPluginSetup; } export interface CallWithRequestParams extends GenericParams { diff --git a/x-pack/plugins/infra/server/lib/compose/kibana.ts b/x-pack/plugins/infra/server/lib/compose/kibana.ts deleted file mode 100644 index 626b9d46bbde3..0000000000000 --- a/x-pack/plugins/infra/server/lib/compose/kibana.ts +++ /dev/null @@ -1,59 +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 { FrameworkFieldsAdapter } from '../adapters/fields/framework_fields_adapter'; -import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; -import { InfraKibanaLogEntriesAdapter } from '../adapters/log_entries/kibana_log_entries_adapter'; -import { KibanaMetricsAdapter } from '../adapters/metrics/kibana_metrics_adapter'; -import { InfraElasticsearchSourceStatusAdapter } from '../adapters/source_status'; -import { InfraFieldsDomain } from '../domains/fields_domain'; -import { InfraLogEntriesDomain } from '../domains/log_entries_domain'; -import { InfraMetricsDomain } from '../domains/metrics_domain'; -import { InfraBackendLibs, InfraDomainLibs } from '../infra_types'; -import { LogEntryCategoriesAnalysis, LogEntryRateAnalysis } from '../log_analysis'; -import { InfraSnapshot } from '../snapshot'; -import { InfraSourceStatus } from '../source_status'; -import { InfraSources } from '../sources'; -import { InfraConfig } from '../../../server'; -import { CoreSetup } from '../../../../../../src/core/server'; -import { InfraServerPluginDeps } from '../adapters/framework/adapter_types'; - -export function compose(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginDeps) { - const framework = new KibanaFramework(core, config, plugins); - const sources = new InfraSources({ - config, - }); - const sourceStatus = new InfraSourceStatus(new InfraElasticsearchSourceStatusAdapter(framework), { - sources, - }); - const snapshot = new InfraSnapshot(); - const logEntryCategoriesAnalysis = new LogEntryCategoriesAnalysis({ framework }); - const logEntryRateAnalysis = new LogEntryRateAnalysis({ framework }); - - // TODO: separate these out individually and do away with "domains" as a temporary group - const domainLibs: InfraDomainLibs = { - fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), { - sources, - }), - logEntries: new InfraLogEntriesDomain(new InfraKibanaLogEntriesAdapter(framework), { - framework, - sources, - }), - metrics: new InfraMetricsDomain(new KibanaMetricsAdapter(framework)), - }; - - const libs: InfraBackendLibs = { - configuration: config, // NP_TODO: Do we ever use this anywhere? - framework, - logEntryCategoriesAnalysis, - logEntryRateAnalysis, - snapshot, - sources, - sourceStatus, - ...domainLibs, - }; - - return libs; -} diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts index 51c433557f4fc..9896ad6ac1cd1 100644 --- a/x-pack/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/plugins/infra/server/lib/infra_types.ts @@ -8,7 +8,6 @@ import { InfraSourceConfiguration } from '../../common/graphql/types'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; import { InfraMetricsDomain } from './domains/metrics_domain'; -import { LogEntryCategoriesAnalysis, LogEntryRateAnalysis } from './log_analysis'; import { InfraSnapshot } from './snapshot'; import { InfraSources } from './sources'; import { InfraSourceStatus } from './source_status'; @@ -31,8 +30,6 @@ export interface InfraDomainLibs { export interface InfraBackendLibs extends InfraDomainLibs { configuration: InfraConfig; framework: KibanaFramework; - logEntryCategoriesAnalysis: LogEntryCategoriesAnalysis; - logEntryRateAnalysis: LogEntryRateAnalysis; snapshot: InfraSnapshot; sources: InfraSources; sourceStatus: InfraSourceStatus; diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts index d0a6ae0fc9357..4298ccb61bbed 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import type { IScopedClusterClient } from 'src/core/server'; import { compareDatasetsByMaximumAnomalyScore, getJobId, @@ -13,7 +13,7 @@ import { } from '../../../common/log_analysis'; import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing'; import { decodeOrThrow } from '../../../common/runtime_types'; -import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; +import type { MlAnomalyDetectors, MlSystem } from '../../types'; import { InsufficientLogAnalysisMlJobConfigurationError, NoLogAnalysisMlJobError, @@ -39,7 +39,6 @@ import { LogEntryDatasetBucket, logEntryDatasetsResponseRT, } from './queries/log_entry_data_sets'; -import { createMlJobsQuery, mlJobsResponseRT } from './queries/ml_jobs'; import { createTopLogEntryCategoriesQuery, topLogEntryCategoriesResponseRT, @@ -47,489 +46,470 @@ import { const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; -export class LogEntryCategoriesAnalysis { - constructor( - private readonly libs: { - framework: KibanaFramework; - } - ) {} - - public async getTopLogEntryCategories( - requestContext: RequestHandlerContext, - request: KibanaRequest, - sourceId: string, - startTime: number, - endTime: number, - categoryCount: number, - datasets: string[], - histograms: HistogramParameters[] - ) { - const finalizeTopLogEntryCategoriesSpan = startTracingSpan('get top categories'); - - const logEntryCategoriesCountJobId = getJobId( - this.libs.framework.getSpaceId(request), - sourceId, - logEntryCategoriesJobTypes[0] - ); - - const { - topLogEntryCategories, - timing: { spans: fetchTopLogEntryCategoriesAggSpans }, - } = await this.fetchTopLogEntryCategories( - requestContext, - logEntryCategoriesCountJobId, - startTime, - endTime, - categoryCount, - datasets - ); - - const categoryIds = topLogEntryCategories.map(({ categoryId }) => categoryId); - - const { - logEntryCategoriesById, - timing: { spans: fetchTopLogEntryCategoryPatternsSpans }, - } = await this.fetchLogEntryCategories( - requestContext, - logEntryCategoriesCountJobId, - categoryIds - ); - - const { - categoryHistogramsById, - timing: { spans: fetchTopLogEntryCategoryHistogramsSpans }, - } = await this.fetchTopLogEntryCategoryHistograms( - requestContext, - logEntryCategoriesCountJobId, - categoryIds, - histograms - ); - - const topLogEntryCategoriesSpan = finalizeTopLogEntryCategoriesSpan(); - - return { - data: topLogEntryCategories.map((topCategory) => ({ - ...topCategory, - regularExpression: logEntryCategoriesById[topCategory.categoryId]?._source.regex ?? '', - histograms: categoryHistogramsById[topCategory.categoryId] ?? [], - })), - timing: { - spans: [ - topLogEntryCategoriesSpan, - ...fetchTopLogEntryCategoriesAggSpans, - ...fetchTopLogEntryCategoryPatternsSpans, - ...fetchTopLogEntryCategoryHistogramsSpans, - ], - }, - }; - } - - public async getLogEntryCategoryDatasets( - requestContext: RequestHandlerContext, - request: KibanaRequest, - sourceId: string, - startTime: number, - endTime: number - ) { - const finalizeLogEntryDatasetsSpan = startTracingSpan('get data sets'); - - const logEntryCategoriesCountJobId = getJobId( - this.libs.framework.getSpaceId(request), - sourceId, - logEntryCategoriesJobTypes[0] - ); - - let logEntryDatasetBuckets: LogEntryDatasetBucket[] = []; - let afterLatestBatchKey: CompositeDatasetKey | undefined; - let esSearchSpans: TracingSpan[] = []; - - while (true) { - const finalizeEsSearchSpan = startTracingSpan('fetch category dataset batch from ES'); - - const logEntryDatasetsResponse = decodeOrThrow(logEntryDatasetsResponseRT)( - await this.libs.framework.callWithRequest( - requestContext, - 'search', - createLogEntryDatasetsQuery( - logEntryCategoriesCountJobId, - startTime, - endTime, - COMPOSITE_AGGREGATION_BATCH_SIZE, - afterLatestBatchKey - ) - ) - ); - - if (logEntryDatasetsResponse._shards.total === 0) { - throw new NoLogAnalysisResultsIndexError( - `Failed to find ml result index for job ${logEntryCategoriesCountJobId}.` - ); - } - - const { - after_key: afterKey, - buckets: latestBatchBuckets, - } = logEntryDatasetsResponse.aggregations.dataset_buckets; - - logEntryDatasetBuckets = [...logEntryDatasetBuckets, ...latestBatchBuckets]; - afterLatestBatchKey = afterKey; - esSearchSpans = [...esSearchSpans, finalizeEsSearchSpan()]; - - if (latestBatchBuckets.length < COMPOSITE_AGGREGATION_BATCH_SIZE) { - break; - } - } - - const logEntryDatasetsSpan = finalizeLogEntryDatasetsSpan(); - - return { - data: logEntryDatasetBuckets.map( - (logEntryDatasetBucket) => logEntryDatasetBucket.key.dataset - ), - timing: { - spans: [logEntryDatasetsSpan, ...esSearchSpans], - }, +export async function getTopLogEntryCategories( + context: { + infra: { + mlSystem: MlSystem; + spaceId: string; }; - } - - public async getLogEntryCategoryExamples( - requestContext: RequestHandlerContext, - request: KibanaRequest, - sourceId: string, - startTime: number, - endTime: number, - categoryId: number, - exampleCount: number - ) { - const finalizeLogEntryCategoryExamplesSpan = startTracingSpan( - 'get category example log entries' - ); - - const logEntryCategoriesCountJobId = getJobId( - this.libs.framework.getSpaceId(request), - sourceId, - logEntryCategoriesJobTypes[0] - ); - - const { - mlJob, - timing: { spans: fetchMlJobSpans }, - } = await this.fetchMlJob(requestContext, logEntryCategoriesCountJobId); - - const customSettings = decodeOrThrow(jobCustomSettingsRT)(mlJob.custom_settings); - const indices = customSettings?.logs_source_config?.indexPattern; - const timestampField = customSettings?.logs_source_config?.timestampField; - - if (indices == null || timestampField == null) { - throw new InsufficientLogAnalysisMlJobConfigurationError( - `Failed to find index configuration for ml job ${logEntryCategoriesCountJobId}` - ); - } - - const { - logEntryCategoriesById, - timing: { spans: fetchLogEntryCategoriesSpans }, - } = await this.fetchLogEntryCategories(requestContext, logEntryCategoriesCountJobId, [ - categoryId, - ]); - const category = logEntryCategoriesById[categoryId]; - - if (category == null) { - throw new UnknownCategoryError(categoryId); - } - - const { - examples, - timing: { spans: fetchLogEntryCategoryExamplesSpans }, - } = await this.fetchLogEntryCategoryExamples( - requestContext, - indices, - timestampField, - startTime, - endTime, - category._source.terms, - exampleCount - ); - - const logEntryCategoryExamplesSpan = finalizeLogEntryCategoryExamplesSpan(); + }, + sourceId: string, + startTime: number, + endTime: number, + categoryCount: number, + datasets: string[], + histograms: HistogramParameters[] +) { + const finalizeTopLogEntryCategoriesSpan = startTracingSpan('get top categories'); + + const logEntryCategoriesCountJobId = getJobId( + context.infra.spaceId, + sourceId, + logEntryCategoriesJobTypes[0] + ); + + const { + topLogEntryCategories, + timing: { spans: fetchTopLogEntryCategoriesAggSpans }, + } = await fetchTopLogEntryCategories( + context, + logEntryCategoriesCountJobId, + startTime, + endTime, + categoryCount, + datasets + ); + + const categoryIds = topLogEntryCategories.map(({ categoryId }) => categoryId); + + const { + logEntryCategoriesById, + timing: { spans: fetchTopLogEntryCategoryPatternsSpans }, + } = await fetchLogEntryCategories(context, logEntryCategoriesCountJobId, categoryIds); + + const { + categoryHistogramsById, + timing: { spans: fetchTopLogEntryCategoryHistogramsSpans }, + } = await fetchTopLogEntryCategoryHistograms( + context, + logEntryCategoriesCountJobId, + categoryIds, + histograms + ); + + const topLogEntryCategoriesSpan = finalizeTopLogEntryCategoriesSpan(); + + return { + data: topLogEntryCategories.map((topCategory) => ({ + ...topCategory, + regularExpression: logEntryCategoriesById[topCategory.categoryId]?._source.regex ?? '', + histograms: categoryHistogramsById[topCategory.categoryId] ?? [], + })), + timing: { + spans: [ + topLogEntryCategoriesSpan, + ...fetchTopLogEntryCategoriesAggSpans, + ...fetchTopLogEntryCategoryPatternsSpans, + ...fetchTopLogEntryCategoryHistogramsSpans, + ], + }, + }; +} - return { - data: examples, - timing: { - spans: [ - logEntryCategoryExamplesSpan, - ...fetchMlJobSpans, - ...fetchLogEntryCategoriesSpans, - ...fetchLogEntryCategoryExamplesSpans, - ], - }, +export async function getLogEntryCategoryDatasets( + context: { + infra: { + mlSystem: MlSystem; + spaceId: string; }; - } - - private async fetchTopLogEntryCategories( - requestContext: RequestHandlerContext, - logEntryCategoriesCountJobId: string, - startTime: number, - endTime: number, - categoryCount: number, - datasets: string[] - ) { - const finalizeEsSearchSpan = startTracingSpan('Fetch top categories from ES'); - - const topLogEntryCategoriesResponse = decodeOrThrow(topLogEntryCategoriesResponseRT)( - await this.libs.framework.callWithRequest( - requestContext, - 'search', - createTopLogEntryCategoriesQuery( + }, + sourceId: string, + startTime: number, + endTime: number +) { + const finalizeLogEntryDatasetsSpan = startTracingSpan('get data sets'); + + const logEntryCategoriesCountJobId = getJobId( + context.infra.spaceId, + sourceId, + logEntryCategoriesJobTypes[0] + ); + + let logEntryDatasetBuckets: LogEntryDatasetBucket[] = []; + let afterLatestBatchKey: CompositeDatasetKey | undefined; + let esSearchSpans: TracingSpan[] = []; + + while (true) { + const finalizeEsSearchSpan = startTracingSpan('fetch category dataset batch from ES'); + + const logEntryDatasetsResponse = decodeOrThrow(logEntryDatasetsResponseRT)( + await context.infra.mlSystem.mlAnomalySearch( + createLogEntryDatasetsQuery( logEntryCategoriesCountJobId, startTime, endTime, - categoryCount, - datasets + COMPOSITE_AGGREGATION_BATCH_SIZE, + afterLatestBatchKey ) ) ); - const esSearchSpan = finalizeEsSearchSpan(); - - if (topLogEntryCategoriesResponse._shards.total === 0) { + if (logEntryDatasetsResponse._shards.total === 0) { throw new NoLogAnalysisResultsIndexError( `Failed to find ml result index for job ${logEntryCategoriesCountJobId}.` ); } - const topLogEntryCategories = topLogEntryCategoriesResponse.aggregations.terms_category_id.buckets.map( - (topCategoryBucket) => { - const maximumAnomalyScoresByDataset = topCategoryBucket.filter_record.terms_dataset.buckets.reduce< - Record - >( - (accumulatedMaximumAnomalyScores, datasetFromRecord) => ({ - ...accumulatedMaximumAnomalyScores, - [datasetFromRecord.key]: datasetFromRecord.maximum_record_score.value ?? 0, - }), - {} - ); - - return { - categoryId: parseCategoryId(topCategoryBucket.key), - logEntryCount: topCategoryBucket.filter_model_plot.sum_actual.value ?? 0, - datasets: topCategoryBucket.filter_model_plot.terms_dataset.buckets - .map((datasetBucket) => ({ - name: datasetBucket.key, - maximumAnomalyScore: maximumAnomalyScoresByDataset[datasetBucket.key] ?? 0, - })) - .sort(compareDatasetsByMaximumAnomalyScore) - .reverse(), - maximumAnomalyScore: topCategoryBucket.filter_record.maximum_record_score.value ?? 0, - }; - } - ); + const { + after_key: afterKey, + buckets: latestBatchBuckets, + } = logEntryDatasetsResponse.aggregations.dataset_buckets; - return { - topLogEntryCategories, - timing: { - spans: [esSearchSpan], - }, + logEntryDatasetBuckets = [...logEntryDatasetBuckets, ...latestBatchBuckets]; + afterLatestBatchKey = afterKey; + esSearchSpans = [...esSearchSpans, finalizeEsSearchSpan()]; + + if (latestBatchBuckets.length < COMPOSITE_AGGREGATION_BATCH_SIZE) { + break; + } + } + + const logEntryDatasetsSpan = finalizeLogEntryDatasetsSpan(); + + return { + data: logEntryDatasetBuckets.map((logEntryDatasetBucket) => logEntryDatasetBucket.key.dataset), + timing: { + spans: [logEntryDatasetsSpan, ...esSearchSpans], + }, + }; +} + +export async function getLogEntryCategoryExamples( + context: { + core: { elasticsearch: { legacy: { client: IScopedClusterClient } } }; + infra: { + mlAnomalyDetectors: MlAnomalyDetectors; + mlSystem: MlSystem; + spaceId: string; }; + }, + sourceId: string, + startTime: number, + endTime: number, + categoryId: number, + exampleCount: number +) { + const finalizeLogEntryCategoryExamplesSpan = startTracingSpan('get category example log entries'); + + const logEntryCategoriesCountJobId = getJobId( + context.infra.spaceId, + sourceId, + logEntryCategoriesJobTypes[0] + ); + + const { + mlJob, + timing: { spans: fetchMlJobSpans }, + } = await fetchMlJob(context, logEntryCategoriesCountJobId); + + const customSettings = decodeOrThrow(jobCustomSettingsRT)(mlJob.custom_settings); + const indices = customSettings?.logs_source_config?.indexPattern; + const timestampField = customSettings?.logs_source_config?.timestampField; + + if (indices == null || timestampField == null) { + throw new InsufficientLogAnalysisMlJobConfigurationError( + `Failed to find index configuration for ml job ${logEntryCategoriesCountJobId}` + ); } - private async fetchLogEntryCategories( - requestContext: RequestHandlerContext, - logEntryCategoriesCountJobId: string, - categoryIds: number[] - ) { - if (categoryIds.length === 0) { - return { - logEntryCategoriesById: {}, - timing: { spans: [] }, - }; - } + const { + logEntryCategoriesById, + timing: { spans: fetchLogEntryCategoriesSpans }, + } = await fetchLogEntryCategories(context, logEntryCategoriesCountJobId, [categoryId]); + const category = logEntryCategoriesById[categoryId]; + + if (category == null) { + throw new UnknownCategoryError(categoryId); + } - const finalizeEsSearchSpan = startTracingSpan('Fetch category patterns from ES'); + const { + examples, + timing: { spans: fetchLogEntryCategoryExamplesSpans }, + } = await fetchLogEntryCategoryExamples( + context, + indices, + timestampField, + startTime, + endTime, + category._source.terms, + exampleCount + ); + + const logEntryCategoryExamplesSpan = finalizeLogEntryCategoryExamplesSpan(); + + return { + data: examples, + timing: { + spans: [ + logEntryCategoryExamplesSpan, + ...fetchMlJobSpans, + ...fetchLogEntryCategoriesSpans, + ...fetchLogEntryCategoryExamplesSpans, + ], + }, + }; +} - const logEntryCategoriesResponse = decodeOrThrow(logEntryCategoriesResponseRT)( - await this.libs.framework.callWithRequest( - requestContext, - 'search', - createLogEntryCategoriesQuery(logEntryCategoriesCountJobId, categoryIds) +async function fetchTopLogEntryCategories( + context: { infra: { mlSystem: MlSystem } }, + logEntryCategoriesCountJobId: string, + startTime: number, + endTime: number, + categoryCount: number, + datasets: string[] +) { + const finalizeEsSearchSpan = startTracingSpan('Fetch top categories from ES'); + + const topLogEntryCategoriesResponse = decodeOrThrow(topLogEntryCategoriesResponseRT)( + await context.infra.mlSystem.mlAnomalySearch( + createTopLogEntryCategoriesQuery( + logEntryCategoriesCountJobId, + startTime, + endTime, + categoryCount, + datasets ) - ); + ) + ); - const esSearchSpan = finalizeEsSearchSpan(); + const esSearchSpan = finalizeEsSearchSpan(); - const logEntryCategoriesById = logEntryCategoriesResponse.hits.hits.reduce< - Record - >( - (accumulatedCategoriesById, categoryHit) => ({ - ...accumulatedCategoriesById, - [categoryHit._source.category_id]: categoryHit, - }), - {} + if (topLogEntryCategoriesResponse._shards.total === 0) { + throw new NoLogAnalysisResultsIndexError( + `Failed to find ml result index for job ${logEntryCategoriesCountJobId}.` ); - - return { - logEntryCategoriesById, - timing: { - spans: [esSearchSpan], - }, - }; } - private async fetchTopLogEntryCategoryHistograms( - requestContext: RequestHandlerContext, - logEntryCategoriesCountJobId: string, - categoryIds: number[], - histograms: HistogramParameters[] - ) { - if (categoryIds.length === 0 || histograms.length === 0) { + const topLogEntryCategories = topLogEntryCategoriesResponse.aggregations.terms_category_id.buckets.map( + (topCategoryBucket) => { + const maximumAnomalyScoresByDataset = topCategoryBucket.filter_record.terms_dataset.buckets.reduce< + Record + >( + (accumulatedMaximumAnomalyScores, datasetFromRecord) => ({ + ...accumulatedMaximumAnomalyScores, + [datasetFromRecord.key]: datasetFromRecord.maximum_record_score.value ?? 0, + }), + {} + ); + return { - categoryHistogramsById: {}, - timing: { spans: [] }, + categoryId: parseCategoryId(topCategoryBucket.key), + logEntryCount: topCategoryBucket.filter_model_plot.sum_actual.value ?? 0, + datasets: topCategoryBucket.filter_model_plot.terms_dataset.buckets + .map((datasetBucket) => ({ + name: datasetBucket.key, + maximumAnomalyScore: maximumAnomalyScoresByDataset[datasetBucket.key] ?? 0, + })) + .sort(compareDatasetsByMaximumAnomalyScore) + .reverse(), + maximumAnomalyScore: topCategoryBucket.filter_record.maximum_record_score.value ?? 0, }; } + ); + + return { + topLogEntryCategories, + timing: { + spans: [esSearchSpan], + }, + }; +} - const finalizeEsSearchSpan = startTracingSpan('Fetch category histograms from ES'); - - const categoryHistogramsReponses = await Promise.all( - histograms.map(({ bucketCount, endTime, id: histogramId, startTime }) => - this.libs.framework - .callWithRequest( - requestContext, - 'search', - createLogEntryCategoryHistogramsQuery( - logEntryCategoriesCountJobId, - categoryIds, - startTime, - endTime, - bucketCount - ) - ) - .then(decodeOrThrow(logEntryCategoryHistogramsResponseRT)) - .then((response) => ({ - histogramId, - histogramBuckets: response.aggregations.filters_categories.buckets, - })) - ) - ); - - const esSearchSpan = finalizeEsSearchSpan(); - - const categoryHistogramsById = Object.values(categoryHistogramsReponses).reduce< - Record< - number, - Array<{ - histogramId: string; - buckets: Array<{ - bucketDuration: number; - logEntryCount: number; - startTime: number; - }>; - }> - > - >( - (outerAccumulatedHistograms, { histogramId, histogramBuckets }) => - Object.entries(histogramBuckets).reduce( - (innerAccumulatedHistograms, [categoryBucketKey, categoryBucket]) => { - const categoryId = parseCategoryId(categoryBucketKey); - return { - ...innerAccumulatedHistograms, - [categoryId]: [ - ...(innerAccumulatedHistograms[categoryId] ?? []), - { - histogramId, - buckets: categoryBucket.histogram_timestamp.buckets.map((bucket) => ({ - bucketDuration: categoryBucket.histogram_timestamp.meta.bucketDuration, - logEntryCount: bucket.sum_actual.value, - startTime: bucket.key, - })), - }, - ], - }; - }, - outerAccumulatedHistograms - ), - {} - ); - +async function fetchLogEntryCategories( + context: { infra: { mlSystem: MlSystem } }, + logEntryCategoriesCountJobId: string, + categoryIds: number[] +) { + if (categoryIds.length === 0) { return { - categoryHistogramsById, - timing: { - spans: [esSearchSpan], - }, + logEntryCategoriesById: {}, + timing: { spans: [] }, }; } - private async fetchMlJob( - requestContext: RequestHandlerContext, - logEntryCategoriesCountJobId: string - ) { - const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES'); - - const { - jobs: [mlJob], - } = decodeOrThrow(mlJobsResponseRT)( - await this.libs.framework.callWithRequest( - requestContext, - 'transport.request', - createMlJobsQuery([logEntryCategoriesCountJobId]) - ) - ); - - const mlGetJobSpan = finalizeMlGetJobSpan(); - - if (mlJob == null) { - throw new NoLogAnalysisMlJobError(`Failed to find ml job ${logEntryCategoriesCountJobId}.`); - } + const finalizeEsSearchSpan = startTracingSpan('Fetch category patterns from ES'); + + const logEntryCategoriesResponse = decodeOrThrow(logEntryCategoriesResponseRT)( + await context.infra.mlSystem.mlAnomalySearch( + createLogEntryCategoriesQuery(logEntryCategoriesCountJobId, categoryIds) + ) + ); + + const esSearchSpan = finalizeEsSearchSpan(); + + const logEntryCategoriesById = logEntryCategoriesResponse.hits.hits.reduce< + Record + >( + (accumulatedCategoriesById, categoryHit) => ({ + ...accumulatedCategoriesById, + [categoryHit._source.category_id]: categoryHit, + }), + {} + ); + + return { + logEntryCategoriesById, + timing: { + spans: [esSearchSpan], + }, + }; +} +async function fetchTopLogEntryCategoryHistograms( + context: { infra: { mlSystem: MlSystem } }, + logEntryCategoriesCountJobId: string, + categoryIds: number[], + histograms: HistogramParameters[] +) { + if (categoryIds.length === 0 || histograms.length === 0) { return { - mlJob, - timing: { - spans: [mlGetJobSpan], - }, + categoryHistogramsById: {}, + timing: { spans: [] }, }; } - private async fetchLogEntryCategoryExamples( - requestContext: RequestHandlerContext, - indices: string, - timestampField: string, - startTime: number, - endTime: number, - categoryQuery: string, - exampleCount: number - ) { - const finalizeEsSearchSpan = startTracingSpan('Fetch examples from ES'); + const finalizeEsSearchSpan = startTracingSpan('Fetch category histograms from ES'); - const { - hits: { hits }, - } = decodeOrThrow(logEntryCategoryExamplesResponseRT)( - await this.libs.framework.callWithRequest( - requestContext, - 'search', - createLogEntryCategoryExamplesQuery( - indices, - timestampField, - startTime, - endTime, - categoryQuery, - exampleCount + const categoryHistogramsReponses = await Promise.all( + histograms.map(({ bucketCount, endTime, id: histogramId, startTime }) => + context.infra.mlSystem + .mlAnomalySearch( + createLogEntryCategoryHistogramsQuery( + logEntryCategoriesCountJobId, + categoryIds, + startTime, + endTime, + bucketCount + ) ) - ) - ); + .then(decodeOrThrow(logEntryCategoryHistogramsResponseRT)) + .then((response) => ({ + histogramId, + histogramBuckets: response.aggregations.filters_categories.buckets, + })) + ) + ); + + const esSearchSpan = finalizeEsSearchSpan(); + + const categoryHistogramsById = Object.values(categoryHistogramsReponses).reduce< + Record< + number, + Array<{ + histogramId: string; + buckets: Array<{ + bucketDuration: number; + logEntryCount: number; + startTime: number; + }>; + }> + > + >( + (outerAccumulatedHistograms, { histogramId, histogramBuckets }) => + Object.entries(histogramBuckets).reduce( + (innerAccumulatedHistograms, [categoryBucketKey, categoryBucket]) => { + const categoryId = parseCategoryId(categoryBucketKey); + return { + ...innerAccumulatedHistograms, + [categoryId]: [ + ...(innerAccumulatedHistograms[categoryId] ?? []), + { + histogramId, + buckets: categoryBucket.histogram_timestamp.buckets.map((bucket) => ({ + bucketDuration: categoryBucket.histogram_timestamp.meta.bucketDuration, + logEntryCount: bucket.sum_actual.value, + startTime: bucket.key, + })), + }, + ], + }; + }, + outerAccumulatedHistograms + ), + {} + ); + + return { + categoryHistogramsById, + timing: { + spans: [esSearchSpan], + }, + }; +} - const esSearchSpan = finalizeEsSearchSpan(); +async function fetchMlJob( + context: { infra: { mlAnomalyDetectors: MlAnomalyDetectors } }, + logEntryCategoriesCountJobId: string +) { + const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES'); - return { - examples: hits.map((hit) => ({ - dataset: hit._source.event?.dataset ?? '', - message: hit._source.message ?? '', - timestamp: hit.sort[0], - })), - timing: { - spans: [esSearchSpan], - }, - }; + const { + jobs: [mlJob], + } = await context.infra.mlAnomalyDetectors.jobs(logEntryCategoriesCountJobId); + + const mlGetJobSpan = finalizeMlGetJobSpan(); + + if (mlJob == null) { + throw new NoLogAnalysisMlJobError(`Failed to find ml job ${logEntryCategoriesCountJobId}.`); } + + return { + mlJob, + timing: { + spans: [mlGetJobSpan], + }, + }; +} + +async function fetchLogEntryCategoryExamples( + requestContext: { core: { elasticsearch: { legacy: { client: IScopedClusterClient } } } }, + indices: string, + timestampField: string, + startTime: number, + endTime: number, + categoryQuery: string, + exampleCount: number +) { + const finalizeEsSearchSpan = startTracingSpan('Fetch examples from ES'); + + const { + hits: { hits }, + } = decodeOrThrow(logEntryCategoryExamplesResponseRT)( + await requestContext.core.elasticsearch.legacy.client.callAsCurrentUser( + 'search', + createLogEntryCategoryExamplesQuery( + indices, + timestampField, + startTime, + endTime, + categoryQuery, + exampleCount + ) + ) + ); + + const esSearchSpan = finalizeEsSearchSpan(); + + return { + examples: hits.map((hit) => ({ + dataset: hit._source.event?.dataset ?? '', + message: hit._source.message ?? '', + timestamp: hit.sort[0], + })), + timing: { + spans: [esSearchSpan], + }, + }; } const parseCategoryId = (rawCategoryId: string) => parseInt(rawCategoryId, 10); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts index 28c1674841973..125cc2b196e09 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts @@ -7,10 +7,8 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { RequestHandlerContext, KibanaRequest } from 'src/core/server'; import { getJobId } from '../../../common/log_analysis'; import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { NoLogAnalysisResultsIndexError } from './errors'; import { logRateModelPlotResponseRT, @@ -18,126 +16,114 @@ import { LogRateModelPlotBucket, CompositeTimestampPartitionKey, } from './queries'; +import { MlSystem } from '../../types'; const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; -export class LogEntryRateAnalysis { - constructor( - private readonly libs: { - framework: KibanaFramework; - } - ) {} - - public getJobIds(request: KibanaRequest, sourceId: string) { - return { - logEntryRate: getJobId(this.libs.framework.getSpaceId(request), sourceId, 'log-entry-rate'), +export async function getLogEntryRateBuckets( + context: { + infra: { + mlSystem: MlSystem; + spaceId: string; }; - } + }, + sourceId: string, + startTime: number, + endTime: number, + bucketDuration: number +) { + const logRateJobId = getJobId(context.infra.spaceId, sourceId, 'log-entry-rate'); + let mlModelPlotBuckets: LogRateModelPlotBucket[] = []; + let afterLatestBatchKey: CompositeTimestampPartitionKey | undefined; - public async getLogEntryRateBuckets( - requestContext: RequestHandlerContext, - request: KibanaRequest, - sourceId: string, - startTime: number, - endTime: number, - bucketDuration: number - ) { - const logRateJobId = this.getJobIds(request, sourceId).logEntryRate; - let mlModelPlotBuckets: LogRateModelPlotBucket[] = []; - let afterLatestBatchKey: CompositeTimestampPartitionKey | undefined; + while (true) { + const mlModelPlotResponse = await context.infra.mlSystem.mlAnomalySearch( + createLogEntryRateQuery( + logRateJobId, + startTime, + endTime, + bucketDuration, + COMPOSITE_AGGREGATION_BATCH_SIZE, + afterLatestBatchKey + ) + ); - while (true) { - const mlModelPlotResponse = await this.libs.framework.callWithRequest( - requestContext, - 'search', - createLogEntryRateQuery( - logRateJobId, - startTime, - endTime, - bucketDuration, - COMPOSITE_AGGREGATION_BATCH_SIZE, - afterLatestBatchKey - ) + if (mlModelPlotResponse._shards.total === 0) { + throw new NoLogAnalysisResultsIndexError( + `Failed to query ml result index for job ${logRateJobId}.` ); + } - if (mlModelPlotResponse._shards.total === 0) { - throw new NoLogAnalysisResultsIndexError( - `Failed to find ml result index for job ${logRateJobId}.` - ); - } - - const { after_key: afterKey, buckets: latestBatchBuckets } = pipe( - logRateModelPlotResponseRT.decode(mlModelPlotResponse), - map((response) => response.aggregations.timestamp_partition_buckets), - fold(throwErrors(createPlainError), identity) - ); + const { after_key: afterKey, buckets: latestBatchBuckets } = pipe( + logRateModelPlotResponseRT.decode(mlModelPlotResponse), + map((response) => response.aggregations.timestamp_partition_buckets), + fold(throwErrors(createPlainError), identity) + ); - mlModelPlotBuckets = [...mlModelPlotBuckets, ...latestBatchBuckets]; - afterLatestBatchKey = afterKey; + mlModelPlotBuckets = [...mlModelPlotBuckets, ...latestBatchBuckets]; + afterLatestBatchKey = afterKey; - if (latestBatchBuckets.length < COMPOSITE_AGGREGATION_BATCH_SIZE) { - break; - } + if (latestBatchBuckets.length < COMPOSITE_AGGREGATION_BATCH_SIZE) { + break; } + } - return mlModelPlotBuckets.reduce< - Array<{ - partitions: Array<{ - analysisBucketCount: number; - anomalies: Array<{ - actualLogEntryRate: number; - anomalyScore: number; - duration: number; - startTime: number; - typicalLogEntryRate: number; - }>; - averageActualLogEntryRate: number; - maximumAnomalyScore: number; - numberOfLogEntries: number; - partitionId: string; + return mlModelPlotBuckets.reduce< + Array<{ + partitions: Array<{ + analysisBucketCount: number; + anomalies: Array<{ + actualLogEntryRate: number; + anomalyScore: number; + duration: number; + startTime: number; + typicalLogEntryRate: number; }>; - startTime: number; - }> - >((histogramBuckets, timestampPartitionBucket) => { - const previousHistogramBucket = histogramBuckets[histogramBuckets.length - 1]; - const partition = { - analysisBucketCount: timestampPartitionBucket.filter_model_plot.doc_count, - anomalies: timestampPartitionBucket.filter_records.top_hits_record.hits.hits.map( - ({ _source: record }) => ({ - actualLogEntryRate: record.actual[0], - anomalyScore: record.record_score, - duration: record.bucket_span * 1000, - startTime: record.timestamp, - typicalLogEntryRate: record.typical[0], - }) - ), - averageActualLogEntryRate: - timestampPartitionBucket.filter_model_plot.average_actual.value || 0, - maximumAnomalyScore: - timestampPartitionBucket.filter_records.maximum_record_score.value || 0, - numberOfLogEntries: timestampPartitionBucket.filter_model_plot.sum_actual.value || 0, - partitionId: timestampPartitionBucket.key.partition, - }; - if ( - previousHistogramBucket && - previousHistogramBucket.startTime === timestampPartitionBucket.key.timestamp - ) { - return [ - ...histogramBuckets.slice(0, -1), - { - ...previousHistogramBucket, - partitions: [...previousHistogramBucket.partitions, partition], - }, - ]; - } else { - return [ - ...histogramBuckets, - { - partitions: [partition], - startTime: timestampPartitionBucket.key.timestamp, - }, - ]; - } - }, []); - } + averageActualLogEntryRate: number; + maximumAnomalyScore: number; + numberOfLogEntries: number; + partitionId: string; + }>; + startTime: number; + }> + >((histogramBuckets, timestampPartitionBucket) => { + const previousHistogramBucket = histogramBuckets[histogramBuckets.length - 1]; + const partition = { + analysisBucketCount: timestampPartitionBucket.filter_model_plot.doc_count, + anomalies: timestampPartitionBucket.filter_records.top_hits_record.hits.hits.map( + ({ _source: record }) => ({ + actualLogEntryRate: record.actual[0], + anomalyScore: record.record_score, + duration: record.bucket_span * 1000, + startTime: record.timestamp, + typicalLogEntryRate: record.typical[0], + }) + ), + averageActualLogEntryRate: + timestampPartitionBucket.filter_model_plot.average_actual.value || 0, + maximumAnomalyScore: timestampPartitionBucket.filter_records.maximum_record_score.value || 0, + numberOfLogEntries: timestampPartitionBucket.filter_model_plot.sum_actual.value || 0, + partitionId: timestampPartitionBucket.key.partition, + }; + if ( + previousHistogramBucket && + previousHistogramBucket.startTime === timestampPartitionBucket.key.timestamp + ) { + return [ + ...histogramBuckets.slice(0, -1), + { + ...previousHistogramBucket, + partitions: [...previousHistogramBucket.partitions, partition], + }, + ]; + } else { + return [ + ...histogramBuckets, + { + partitions: [partition], + startTime: timestampPartitionBucket.key.timestamp, + }, + ]; + } + }, []); } diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/common.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/common.ts index f1e68d34fdae3..eacf29b303db0 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/common.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/common.ts @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -const ML_ANOMALY_INDEX_PREFIX = '.ml-anomalies-'; - -export const getMlResultIndex = (jobId: string) => `${ML_ANOMALY_INDEX_PREFIX}${jobId}`; - export const defaultRequestParameters = { allowNoIndices: true, ignoreUnavailable: true, @@ -15,6 +11,16 @@ export const defaultRequestParameters = { trackTotalHits: false, }; +export const createJobIdFilters = (jobId: string) => [ + { + term: { + job_id: { + value: jobId, + }, + }, + }, +]; + export const createTimeRangeFilters = (startTime: number, endTime: number) => [ { range: { @@ -26,12 +32,10 @@ export const createTimeRangeFilters = (startTime: number, endTime: number) => [ }, ]; -export const createResultTypeFilters = (resultType: 'model_plot' | 'record') => [ +export const createResultTypeFilters = (resultTypes: Array<'model_plot' | 'record'>) => [ { - term: { - result_type: { - value: resultType, - }, + terms: { + result_type: resultTypes, }, }, ]; diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_categories.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_categories.ts index 2681a4c037f5d..c7ad60eeaabc2 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_categories.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_categories.ts @@ -5,9 +5,8 @@ */ import * as rt from 'io-ts'; - import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; -import { defaultRequestParameters, getMlResultIndex, createCategoryIdFilters } from './common'; +import { createCategoryIdFilters, createJobIdFilters, defaultRequestParameters } from './common'; export const createLogEntryCategoriesQuery = ( logEntryCategoriesJobId: string, @@ -17,12 +16,14 @@ export const createLogEntryCategoriesQuery = ( body: { query: { bool: { - filter: [...createCategoryIdFilters(categoryIds)], + filter: [ + ...createJobIdFilters(logEntryCategoriesJobId), + ...createCategoryIdFilters(categoryIds), + ], }, }, _source: ['category_id', 'regex', 'terms'], }, - index: getMlResultIndex(logEntryCategoriesJobId), size: categoryIds.length, }); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_category_histograms.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_category_histograms.ts index 67087f3b4775b..5fdafb5123251 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_category_histograms.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_category_histograms.ts @@ -5,13 +5,12 @@ */ import * as rt from 'io-ts'; - import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { + createJobIdFilters, createResultTypeFilters, createTimeRangeFilters, defaultRequestParameters, - getMlResultIndex, } from './common'; export const createLogEntryCategoryHistogramsQuery = ( @@ -26,8 +25,9 @@ export const createLogEntryCategoryHistogramsQuery = ( query: { bool: { filter: [ + ...createJobIdFilters(logEntryCategoriesJobId), ...createTimeRangeFilters(startTime, endTime), - ...createResultTypeFilters('model_plot'), + ...createResultTypeFilters(['model_plot']), ...createCategoryFilters(categoryIds), ], }, @@ -41,7 +41,6 @@ export const createLogEntryCategoryHistogramsQuery = ( }, }, }, - index: getMlResultIndex(logEntryCategoriesJobId), size: 0, }); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_data_sets.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_data_sets.ts index b41a21a21b6a6..dd22bedae8b2a 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_data_sets.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_data_sets.ts @@ -5,9 +5,13 @@ */ import * as rt from 'io-ts'; - import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; -import { defaultRequestParameters, getMlResultIndex } from './common'; +import { + createJobIdFilters, + createResultTypeFilters, + createTimeRangeFilters, + defaultRequestParameters, +} from './common'; export const createLogEntryDatasetsQuery = ( logEntryAnalysisJobId: string, @@ -21,21 +25,9 @@ export const createLogEntryDatasetsQuery = ( query: { bool: { filter: [ - { - range: { - timestamp: { - gte: startTime, - lt: endTime, - }, - }, - }, - { - term: { - result_type: { - value: 'model_plot', - }, - }, - }, + ...createJobIdFilters(logEntryAnalysisJobId), + ...createTimeRangeFilters(startTime, endTime), + ...createResultTypeFilters(['model_plot']), ], }, }, @@ -58,7 +50,6 @@ export const createLogEntryDatasetsQuery = ( }, }, }, - index: getMlResultIndex(logEntryAnalysisJobId), size: 0, }); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts index def7caf578b94..269850e292636 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_rate.ts @@ -5,8 +5,12 @@ */ import * as rt from 'io-ts'; - -import { defaultRequestParameters, getMlResultIndex } from './common'; +import { + createJobIdFilters, + createResultTypeFilters, + createTimeRangeFilters, + defaultRequestParameters, +} from './common'; export const createLogEntryRateQuery = ( logRateJobId: string, @@ -21,19 +25,9 @@ export const createLogEntryRateQuery = ( query: { bool: { filter: [ - { - range: { - timestamp: { - gte: startTime, - lt: endTime, - }, - }, - }, - { - terms: { - result_type: ['model_plot', 'record'], - }, - }, + ...createJobIdFilters(logRateJobId), + ...createTimeRangeFilters(startTime, endTime), + ...createResultTypeFilters(['model_plot', 'record']), { term: { detector_index: { @@ -118,7 +112,6 @@ export const createLogEntryRateQuery = ( }, }, }, - index: getMlResultIndex(logRateJobId), size: 0, }); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts index 517d31865e358..6fa7156240508 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/top_log_entry_categories.ts @@ -5,13 +5,12 @@ */ import * as rt from 'io-ts'; - import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { + createJobIdFilters, createResultTypeFilters, createTimeRangeFilters, defaultRequestParameters, - getMlResultIndex, } from './common'; export const createTopLogEntryCategoriesQuery = ( @@ -27,6 +26,7 @@ export const createTopLogEntryCategoriesQuery = ( query: { bool: { filter: [ + ...createJobIdFilters(logEntryCategoriesJobId), ...createTimeRangeFilters(startTime, endTime), ...createDatasetsFilters(datasets), { @@ -35,7 +35,7 @@ export const createTopLogEntryCategoriesQuery = ( { bool: { filter: [ - ...createResultTypeFilters('model_plot'), + ...createResultTypeFilters(['model_plot']), { range: { actual: { @@ -48,7 +48,7 @@ export const createTopLogEntryCategoriesQuery = ( }, { bool: { - filter: createResultTypeFilters('record'), + filter: createResultTypeFilters(['record']), }, }, ], @@ -119,7 +119,6 @@ export const createTopLogEntryCategoriesQuery = ( }, }, }, - index: getMlResultIndex(logEntryCategoriesJobId), size: 0, }); diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 2fd614830c05d..8062c48d98617 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -19,7 +19,6 @@ import { InfraElasticsearchSourceStatusAdapter } from './lib/adapters/source_sta import { InfraFieldsDomain } from './lib/domains/fields_domain'; import { InfraLogEntriesDomain } from './lib/domains/log_entries_domain'; import { InfraMetricsDomain } from './lib/domains/metrics_domain'; -import { LogEntryCategoriesAnalysis, LogEntryRateAnalysis } from './lib/log_analysis'; import { InfraSnapshot } from './lib/snapshot'; import { InfraSourceStatus } from './lib/source_status'; import { InfraSources } from './lib/sources'; @@ -31,6 +30,7 @@ import { registerAlertTypes } from './lib/alerting'; import { infraSourceConfigurationSavedObjectType } from './lib/sources'; import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; +import { InfraRequestHandlerContext } from './types'; export const config = { schema: schema.object({ @@ -106,8 +106,6 @@ export class InfraServerPlugin { } ); const snapshot = new InfraSnapshot(); - const logEntryCategoriesAnalysis = new LogEntryCategoriesAnalysis({ framework }); - const logEntryRateAnalysis = new LogEntryRateAnalysis({ framework }); // register saved object types core.savedObjects.registerType(infraSourceConfigurationSavedObjectType); @@ -115,6 +113,8 @@ export class InfraServerPlugin { core.savedObjects.registerType(inventoryViewSavedObjectType); // TODO: separate these out individually and do away with "domains" as a temporary group + // and make them available via the request context so we can do away with + // the wrapper classes const domainLibs: InfraDomainLibs = { fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), { sources, @@ -129,8 +129,6 @@ export class InfraServerPlugin { this.libs = { configuration: this.config, framework, - logEntryCategoriesAnalysis, - logEntryRateAnalysis, snapshot, sources, sourceStatus, @@ -151,6 +149,25 @@ export class InfraServerPlugin { initInfraServer(this.libs); registerAlertTypes(plugins.alerts, this.libs); + core.http.registerRouteHandlerContext( + 'infra', + (context, request): InfraRequestHandlerContext => { + const mlSystem = + context.ml && + plugins.ml?.mlSystemProvider(context.ml?.mlClient.callAsCurrentUser, request); + const mlAnomalyDetectors = + context.ml && + plugins.ml?.anomalyDetectorsProvider(context.ml?.mlClient.callAsCurrentUser); + const spaceId = plugins.spaces?.spacesService.getSpaceId(request) || 'default'; + + return { + mlAnomalyDetectors, + mlSystem, + spaceId, + }; + } + ); + // Telemetry UsageCollector.registerUsageCollector(plugins.usageCollection); diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts index d335774c85f38..f9f31f28dffeb 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_categories.ts @@ -5,36 +5,29 @@ */ import Boom from 'boom'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { schema } from '@kbn/config-schema'; -import { InfraBackendLibs } from '../../../lib/infra_types'; import { - LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH, getLogEntryCategoriesRequestPayloadRT, getLogEntryCategoriesSuccessReponsePayloadRT, + LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH, } from '../../../../common/http_api/log_analysis'; -import { throwErrors } from '../../../../common/runtime_types'; -import { NoLogAnalysisResultsIndexError } from '../../../lib/log_analysis'; - -const anyObject = schema.object({}, { unknowns: 'allow' }); +import { createValidationFunction } from '../../../../common/runtime_types'; +import type { InfraBackendLibs } from '../../../lib/infra_types'; +import { + getTopLogEntryCategories, + NoLogAnalysisResultsIndexError, +} from '../../../lib/log_analysis'; +import { assertHasInfraMlPlugins } from '../../../utils/request_context'; -export const initGetLogEntryCategoriesRoute = ({ - framework, - logEntryCategoriesAnalysis, -}: InfraBackendLibs) => { +export const initGetLogEntryCategoriesRoute = ({ framework }: InfraBackendLibs) => { framework.registerRoute( { method: 'post', path: LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORIES_PATH, validate: { - // short-circuit forced @kbn/config-schema validation so we can do io-ts validation - body: anyObject, + body: createValidationFunction(getLogEntryCategoriesRequestPayloadRT), }, }, - async (requestContext, request, response) => { + framework.router.handleLegacyErrors(async (requestContext, request, response) => { const { data: { categoryCount, @@ -43,18 +36,13 @@ export const initGetLogEntryCategoriesRoute = ({ timeRange: { startTime, endTime }, datasets, }, - } = pipe( - getLogEntryCategoriesRequestPayloadRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + } = request.body; try { - const { - data: topLogEntryCategories, - timing, - } = await logEntryCategoriesAnalysis.getTopLogEntryCategories( + assertHasInfraMlPlugins(requestContext); + + const { data: topLogEntryCategories, timing } = await getTopLogEntryCategories( requestContext, - request, sourceId, startTime, endTime, @@ -76,18 +64,22 @@ export const initGetLogEntryCategoriesRoute = ({ timing, }), }); - } catch (e) { - const { statusCode = 500, message = 'Unknown error occurred' } = e; + } catch (error) { + if (Boom.isBoom(error)) { + throw error; + } - if (e instanceof NoLogAnalysisResultsIndexError) { - return response.notFound({ body: { message } }); + if (error instanceof NoLogAnalysisResultsIndexError) { + return response.notFound({ body: { message: error.message } }); } return response.customError({ - statusCode, - body: { message }, + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, }); } - } + }) ); }; diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_datasets.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_datasets.ts index 730e32dee2fbe..69b1e942464fd 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_datasets.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_datasets.ts @@ -4,54 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; import Boom from 'boom'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { pipe } from 'fp-ts/lib/pipeable'; - import { getLogEntryCategoryDatasetsRequestPayloadRT, getLogEntryCategoryDatasetsSuccessReponsePayloadRT, LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORY_DATASETS_PATH, } from '../../../../common/http_api/log_analysis'; -import { throwErrors } from '../../../../common/runtime_types'; -import { InfraBackendLibs } from '../../../lib/infra_types'; -import { NoLogAnalysisResultsIndexError } from '../../../lib/log_analysis'; - -const anyObject = schema.object({}, { unknowns: 'allow' }); +import { createValidationFunction } from '../../../../common/runtime_types'; +import type { InfraBackendLibs } from '../../../lib/infra_types'; +import { + getLogEntryCategoryDatasets, + NoLogAnalysisResultsIndexError, +} from '../../../lib/log_analysis'; +import { assertHasInfraMlPlugins } from '../../../utils/request_context'; -export const initGetLogEntryCategoryDatasetsRoute = ({ - framework, - logEntryCategoriesAnalysis, -}: InfraBackendLibs) => { +export const initGetLogEntryCategoryDatasetsRoute = ({ framework }: InfraBackendLibs) => { framework.registerRoute( { method: 'post', path: LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORY_DATASETS_PATH, validate: { - // short-circuit forced @kbn/config-schema validation so we can do io-ts validation - body: anyObject, + body: createValidationFunction(getLogEntryCategoryDatasetsRequestPayloadRT), }, }, - async (requestContext, request, response) => { + framework.router.handleLegacyErrors(async (requestContext, request, response) => { const { data: { sourceId, timeRange: { startTime, endTime }, }, - } = pipe( - getLogEntryCategoryDatasetsRequestPayloadRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + } = request.body; try { - const { - data: logEntryCategoryDatasets, - timing, - } = await logEntryCategoriesAnalysis.getLogEntryCategoryDatasets( + assertHasInfraMlPlugins(requestContext); + + const { data: logEntryCategoryDatasets, timing } = await getLogEntryCategoryDatasets( requestContext, - request, sourceId, startTime, endTime @@ -65,18 +53,22 @@ export const initGetLogEntryCategoryDatasetsRoute = ({ timing, }), }); - } catch (e) { - const { statusCode = 500, message = 'Unknown error occurred' } = e; + } catch (error) { + if (Boom.isBoom(error)) { + throw error; + } - if (e instanceof NoLogAnalysisResultsIndexError) { - return response.notFound({ body: { message } }); + if (error instanceof NoLogAnalysisResultsIndexError) { + return response.notFound({ body: { message: error.message } }); } return response.customError({ - statusCode, - body: { message }, + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, }); } - } + }) ); }; diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts index 44f466cc77c89..217180c0290f7 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_category_examples.ts @@ -4,37 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; import Boom from 'boom'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { pipe } from 'fp-ts/lib/pipeable'; - import { getLogEntryCategoryExamplesRequestPayloadRT, getLogEntryCategoryExamplesSuccessReponsePayloadRT, LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORY_EXAMPLES_PATH, } from '../../../../common/http_api/log_analysis'; -import { throwErrors } from '../../../../common/runtime_types'; -import { InfraBackendLibs } from '../../../lib/infra_types'; -import { NoLogAnalysisResultsIndexError } from '../../../lib/log_analysis'; - -const anyObject = schema.object({}, { unknowns: 'allow' }); +import { createValidationFunction } from '../../../../common/runtime_types'; +import type { InfraBackendLibs } from '../../../lib/infra_types'; +import { + getLogEntryCategoryExamples, + NoLogAnalysisResultsIndexError, +} from '../../../lib/log_analysis'; +import { assertHasInfraMlPlugins } from '../../../utils/request_context'; -export const initGetLogEntryCategoryExamplesRoute = ({ - framework, - logEntryCategoriesAnalysis, -}: InfraBackendLibs) => { +export const initGetLogEntryCategoryExamplesRoute = ({ framework }: InfraBackendLibs) => { framework.registerRoute( { method: 'post', path: LOG_ANALYSIS_GET_LOG_ENTRY_CATEGORY_EXAMPLES_PATH, validate: { - // short-circuit forced @kbn/config-schema validation so we can do io-ts validation - body: anyObject, + body: createValidationFunction(getLogEntryCategoryExamplesRequestPayloadRT), }, }, - async (requestContext, request, response) => { + framework.router.handleLegacyErrors(async (requestContext, request, response) => { const { data: { categoryId, @@ -42,18 +35,13 @@ export const initGetLogEntryCategoryExamplesRoute = ({ sourceId, timeRange: { startTime, endTime }, }, - } = pipe( - getLogEntryCategoryExamplesRequestPayloadRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + } = request.body; try { - const { - data: logEntryCategoryExamples, - timing, - } = await logEntryCategoriesAnalysis.getLogEntryCategoryExamples( + assertHasInfraMlPlugins(requestContext); + + const { data: logEntryCategoryExamples, timing } = await getLogEntryCategoryExamples( requestContext, - request, sourceId, startTime, endTime, @@ -69,18 +57,22 @@ export const initGetLogEntryCategoryExamplesRoute = ({ timing, }), }); - } catch (e) { - const { statusCode = 500, message = 'Unknown error occurred' } = e; + } catch (error) { + if (Boom.isBoom(error)) { + throw error; + } - if (e instanceof NoLogAnalysisResultsIndexError) { - return response.notFound({ body: { message } }); + if (error instanceof NoLogAnalysisResultsIndexError) { + return response.notFound({ body: { message: error.message } }); } return response.customError({ - statusCode, - body: { message }, + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, }); } - } + }) ); }; diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts index 38dc0a790a7a3..ae86102980c16 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts @@ -5,11 +5,6 @@ */ import Boom from 'boom'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../../lib/infra_types'; import { LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, @@ -17,57 +12,61 @@ import { getLogEntryRateSuccessReponsePayloadRT, GetLogEntryRateSuccessResponsePayload, } from '../../../../common/http_api/log_analysis'; -import { throwErrors } from '../../../../common/runtime_types'; -import { NoLogAnalysisResultsIndexError } from '../../../lib/log_analysis'; - -const anyObject = schema.object({}, { unknowns: 'allow' }); +import { createValidationFunction } from '../../../../common/runtime_types'; +import { NoLogAnalysisResultsIndexError, getLogEntryRateBuckets } from '../../../lib/log_analysis'; +import { assertHasInfraMlPlugins } from '../../../utils/request_context'; -export const initGetLogEntryRateRoute = ({ framework, logEntryRateAnalysis }: InfraBackendLibs) => { +export const initGetLogEntryRateRoute = ({ framework }: InfraBackendLibs) => { framework.registerRoute( { method: 'post', path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, validate: { - // short-circuit forced @kbn/config-schema validation so we can do io-ts validation - body: anyObject, + body: createValidationFunction(getLogEntryRateRequestPayloadRT), }, }, - async (requestContext, request, response) => { + framework.router.handleLegacyErrors(async (requestContext, request, response) => { + const { + data: { sourceId, timeRange, bucketDuration }, + } = request.body; + try { - const payload = pipe( - getLogEntryRateRequestPayloadRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + assertHasInfraMlPlugins(requestContext); - const logEntryRateBuckets = await logEntryRateAnalysis.getLogEntryRateBuckets( + const logEntryRateBuckets = await getLogEntryRateBuckets( requestContext, - request, - payload.data.sourceId, - payload.data.timeRange.startTime, - payload.data.timeRange.endTime, - payload.data.bucketDuration + sourceId, + timeRange.startTime, + timeRange.endTime, + bucketDuration ); return response.ok({ body: getLogEntryRateSuccessReponsePayloadRT.encode({ data: { - bucketDuration: payload.data.bucketDuration, + bucketDuration, histogramBuckets: logEntryRateBuckets, totalNumberOfLogEntries: getTotalNumberOfLogEntries(logEntryRateBuckets), }, }), }); - } catch (e) { - const { statusCode = 500, message = 'Unknown error occurred' } = e; - if (e instanceof NoLogAnalysisResultsIndexError) { - return response.notFound({ body: { message } }); + } catch (error) { + if (Boom.isBoom(error)) { + throw error; } + + if (error instanceof NoLogAnalysisResultsIndexError) { + return response.notFound({ body: { message: error.message } }); + } + return response.customError({ - statusCode, - body: { message }, + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, }); } - } + }) ); }; diff --git a/x-pack/plugins/infra/server/types.ts b/x-pack/plugins/infra/server/types.ts new file mode 100644 index 0000000000000..735569a790f64 --- /dev/null +++ b/x-pack/plugins/infra/server/types.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MlPluginSetup } from '../../ml/server'; + +export type MlSystem = ReturnType; +export type MlAnomalyDetectors = ReturnType; + +export interface InfraMlRequestHandlerContext { + mlAnomalyDetectors?: MlAnomalyDetectors; + mlSystem?: MlSystem; +} + +export interface InfraSpacesRequestHandlerContext { + spaceId: string; +} + +export type InfraRequestHandlerContext = InfraMlRequestHandlerContext & + InfraSpacesRequestHandlerContext; + +declare module 'src/core/server' { + interface RequestHandlerContext { + infra?: InfraRequestHandlerContext; + } +} diff --git a/x-pack/plugins/infra/server/utils/request_context.ts b/x-pack/plugins/infra/server/utils/request_context.ts new file mode 100644 index 0000000000000..30855d74d9e30 --- /dev/null +++ b/x-pack/plugins/infra/server/utils/request_context.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable max-classes-per-file */ + +import { InfraMlRequestHandlerContext, InfraRequestHandlerContext } from '../types'; + +export class MissingContextValuesError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class NoMlPluginError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export function assertHasInfraPlugins( + context: Context +): asserts context is Context & { infra: Context['infra'] } { + if (context.infra == null) { + throw new MissingContextValuesError('Failed to access "infra" context values.'); + } +} + +export function assertHasInfraMlPlugins( + context: Context +): asserts context is Context & { + infra: Context['infra'] & Required; +} { + assertHasInfraPlugins(context); + + if (context.infra?.mlAnomalyDetectors == null || context.infra?.mlSystem == null) { + throw new NoMlPluginError('Failed to access ML plugin.'); + } +} diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts b/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts index 172e582e40de5..7bcea4c17cdcd 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts @@ -37,7 +37,7 @@ export default ({ getService }: FtrProviderContext) => { before(() => esArchiver.load('empty_kibana')); after(() => esArchiver.unload('empty_kibana')); - it('should return buckets when the results index exists with matching documents', async () => { + it('should return buckets when there are matching ml result documents', async () => { const { body } = await supertest .post(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH) .set(COMMON_HEADERS) @@ -68,7 +68,7 @@ export default ({ getService }: FtrProviderContext) => { ).to.be(true); }); - it('should return no buckets when the results index exists without matching documents', async () => { + it('should return no buckets when there are no matching ml result documents', async () => { const { body } = await supertest .post(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH) .set(COMMON_HEADERS) @@ -78,7 +78,7 @@ export default ({ getService }: FtrProviderContext) => { sourceId: 'default', timeRange: { startTime: TIME_BEFORE_START - 10 * 15 * 60 * 1000, - endTime: TIME_BEFORE_START, + endTime: TIME_BEFORE_START - 1, }, bucketDuration: 15 * 60 * 1000, }, @@ -94,25 +94,6 @@ export default ({ getService }: FtrProviderContext) => { expect(logEntryRateBuckets.data.bucketDuration).to.be(15 * 60 * 1000); expect(logEntryRateBuckets.data.histogramBuckets).to.be.empty(); }); - - it('should return a NotFound error when the results index does not exist', async () => { - await supertest - .post(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH) - .set(COMMON_HEADERS) - .send( - getLogEntryRateRequestPayloadRT.encode({ - data: { - sourceId: 'does-not-exist', - timeRange: { - startTime: TIME_BEFORE_START, - endTime: TIME_AFTER_END, - }, - bucketDuration: 15 * 60 * 1000, - }, - }) - ) - .expect(404); - }); }); }); }); From fb8e9e82801e1afcb9ac65a691110b0a70d039d5 Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Thu, 25 Jun 2020 10:59:49 -0400 Subject: [PATCH 36/82] Makes usage collection methods available on start (#69836) (#69924) --- src/plugins/usage_collection/public/plugin.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/plugins/usage_collection/public/plugin.ts b/src/plugins/usage_collection/public/plugin.ts index cf2f6af1507c0..40f27f8269928 100644 --- a/src/plugins/usage_collection/public/plugin.ts +++ b/src/plugins/usage_collection/public/plugin.ts @@ -52,12 +52,17 @@ export interface UsageCollectionSetup { }; } +export interface UsageCollectionStart { + reportUiStats: Reporter['reportUiStats']; + METRIC_TYPE: typeof METRIC_TYPE; +} + export function isUnauthenticated(http: HttpSetup) { const { anonymousPaths } = http; return anonymousPaths.isAnonymous(window.location.pathname); } -export class UsageCollectionPlugin implements Plugin { +export class UsageCollectionPlugin implements Plugin { private readonly legacyAppId$ = new Subject(); private trackUserAgent: boolean = true; private reporter?: Reporter; @@ -90,7 +95,7 @@ export class UsageCollectionPlugin implements Plugin { public start({ http, application }: CoreStart) { if (!this.reporter) { - return; + throw new Error('Usage collection reporter not set up correctly'); } if (this.config.uiMetric.enabled && !isUnauthenticated(http)) { @@ -100,7 +105,13 @@ export class UsageCollectionPlugin implements Plugin { if (this.trackUserAgent) { this.reporter.reportUserAgent('kibana'); } + reportApplicationUsage(merge(application.currentAppId$, this.legacyAppId$), this.reporter); + + return { + reportUiStats: this.reporter.reportUiStats, + METRIC_TYPE, + }; } public stop() {} From 8bc04a98ae4bcb00d0d879a335e2286de5770964 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 25 Jun 2020 18:00:42 +0300 Subject: [PATCH 37/82] [7.x] [SIEM][CASE] Improve Jira's labelling (#69892) (#69923) * Change labeling * Improve word --- .../common/lib/connectors/jira/flyout.tsx | 4 +-- .../common/lib/connectors/jira/index.tsx | 4 +-- .../lib/connectors/jira/translations.ts | 28 +++++++++++++++++++ .../common/lib/connectors/translations.ts | 4 +-- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx index c9953fdb30e02..0737db3cd08eb 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx @@ -63,7 +63,7 @@ const JiraConnectorForm: React.FC> fullWidth error={errors.email} isInvalid={isEmailInvalid} - label={i18n.EMAIL_LABEL} + label={i18n.JIRA_EMAIL_LABEL} > > fullWidth error={errors.apiToken} isInvalid={isApiTokenInvalid} - label={i18n.API_TOKEN_LABEL} + label={i18n.JIRA_API_TOKEN_LABEL} > { } if (!action.secrets.email) { - errors.email = [...errors.email, i18n.EMAIL_REQUIRED]; + errors.email = [...errors.email, i18n.JIRA_EMAIL_REQUIRED]; } if (!action.secrets.apiToken) { - errors.apiToken = [...errors.apiToken, i18n.API_TOKEN_REQUIRED]; + errors.apiToken = [...errors.apiToken, i18n.JIRA_API_TOKEN_REQUIRED]; } return { errors }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts index 286f81842411b..bcb2c49a0de74 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts @@ -36,6 +36,34 @@ export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate( } ); +export const JIRA_EMAIL_LABEL = i18n.translate( + 'xpack.securitySolution.case.connectors.jira.emailTextFieldLabel', + { + defaultMessage: 'Email or Username', + } +); + +export const JIRA_EMAIL_REQUIRED = i18n.translate( + 'xpack.securitySolution.case.connectors.jira.requiredEmailTextField', + { + defaultMessage: 'Email or Username is required', + } +); + +export const JIRA_API_TOKEN_LABEL = i18n.translate( + 'xpack.securitySolution.case.connectors.jira.apiTokenTextFieldLabel', + { + defaultMessage: 'API token or Password', + } +); + +export const JIRA_API_TOKEN_REQUIRED = i18n.translate( + 'xpack.securitySolution.case.connectors.jira.requiredApiTokenTextField', + { + defaultMessage: 'API token or Password is required', + } +); + export const MAPPING_FIELD_SUMMARY = i18n.translate( 'xpack.securitySolution.case.configureCases.mappingFieldSummary', { diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/translations.ts index 40848ea769008..6dd1247d40fcb 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/translations.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/translations.ts @@ -58,14 +58,14 @@ export const PASSWORD_REQUIRED = i18n.translate( export const API_TOKEN_LABEL = i18n.translate( 'xpack.securitySolution.case.connectors.common.apiTokenTextFieldLabel', { - defaultMessage: 'Api token', + defaultMessage: 'API token', } ); export const API_TOKEN_REQUIRED = i18n.translate( 'xpack.securitySolution.case.connectors.common.requiredApiTokenTextField', { - defaultMessage: 'Api token is required', + defaultMessage: 'API token is required', } ); From 0138448526143bd2d4dd549f6fa7da9d0430ecb3 Mon Sep 17 00:00:00 2001 From: Joel Griffith Date: Thu, 25 Jun 2020 08:20:56 -0700 Subject: [PATCH 38/82] Don't set a min-length on encryption key for reportin (#69827) (#69859) --- x-pack/plugins/reporting/server/config/schema.test.ts | 2 ++ x-pack/plugins/reporting/server/config/schema.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts index 41285c2bfa133..ddd5491b661bc 100644 --- a/x-pack/plugins/reporting/server/config/schema.test.ts +++ b/x-pack/plugins/reporting/server/config/schema.test.ts @@ -112,6 +112,8 @@ describe('Reporting Config Schema', () => { .encryptionKey ).toBe('qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'); + expect(ConfigSchema.validate({ encryptionKey: 'weaksauce' }).encryptionKey).toBe('weaksauce'); + // disableSandbox expect( ConfigSchema.validate({ capture: { browser: { chromium: { disableSandbox: true } } } }) diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index b1234a6ddf0b6..2f77aff0020d5 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -136,8 +136,8 @@ const CsvSchema = schema.object({ const EncryptionKeySchema = schema.conditional( schema.contextRef('dist'), true, - schema.maybe(schema.string({ minLength: 32 })), // default value is dynamic in createConfig$ - schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) }) + schema.maybe(schema.string()), // default value is dynamic in createConfig$ + schema.string({ defaultValue: 'a'.repeat(32) }) ); const RolesSchema = schema.object({ From 0a30e34a414a7b0db556d59e9966ef288ef3f5c0 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 25 Jun 2020 11:21:51 -0400 Subject: [PATCH 39/82] [SIEM][Detection Engine] - Update DE to work with new exceptions schema (#69715) (#69932) ## Summary The purpose of this PR is to update the detection engine to work with the new exception list schema. Previously, exceptions lived directly in the rule, now they are stored separately, and the rule only stores a reference to said exception lists. Changes are concentrated in two areas. First `x-pack/plugins/lists`: - Updates the `EntryList` schema so it now takes a list `id` and `type` (as needed for the detections engine logic) as opposed to before where it was just a string for the `id` - Updates the lists plugin unit tests to account for the updated `EntryList` schema - Exposes the exceptions list client (in the lists plugin) to be used by the detection engine Second in `x-pack/plugins/security_solution`: - Updates the detection engine `exceptions_list` schema. Previously, exceptions sat directly on the rule itself, now that exceptions are stored separately, the `exceptions_list` property needs to only keep reference to the exceptions list - Updates schema unit tests - Updates routes to use new exceptions list schema - Updates route unit tests - Removes old exception list scripts that are no longer necessary --- x-pack/plugins/lists/README.md | 6 +- x-pack/plugins/lists/common/constants.mock.ts | 6 +- .../types/default_entries_array.test.ts | 3 +- .../schemas/types/default_namespace.test.ts | 61 + .../common/schemas/types/default_namespace.ts | 2 +- .../common/schemas/types/entries.mock.ts | 6 +- .../common/schemas/types/entries.test.ts | 22 +- .../lists/common/schemas/types/entries.ts | 6 +- .../lists/common/schemas/types/index.ts | 1 + x-pack/plugins/lists/server/index.ts | 1 + .../server/saved_objects/exception_list.ts | 10 + .../new/exception_list_item_with_list.json | 24 + .../scripts/lists/new/list_ip_item.json | 2 +- .../detection_engine/lists_common_deps.ts | 7 + .../schemas/common/schemas.ts | 37 - .../request/add_prepackaged_rules_schema.ts | 27 +- .../add_prepackged_rules_schema.test.ts | 188 ++- .../request/create_rules_schema.test.ts | 184 ++- .../schemas/request/create_rules_schema.ts | 31 +- .../request/import_rules_schema.test.ts | 187 ++- .../schemas/request/import_rules_schema.ts | 33 +- .../request/patch_rules_schema.test.ts | 155 ++- .../schemas/request/patch_rules_schema.ts | 4 +- .../request/update_rules_schema.test.ts | 181 ++- .../schemas/request/update_rules_schema.ts | 27 +- .../schemas/response/rules_schema.mocks.ts | 34 +- .../schemas/response/rules_schema.test.ts | 44 + .../schemas/response/rules_schema.ts | 4 +- .../detection_engine/schemas/types/index.ts | 34 + .../schemas/types/lists.mock.ts | 18 + .../schemas/types/lists.test.ts | 131 ++ .../detection_engine/schemas/types/lists.ts | 22 + .../schemas/types/lists_default_array.test.ts | 173 +-- .../schemas/types/lists_default_array.ts | 28 +- .../components/exceptions/helpers.test.tsx | 6 +- .../common/components/exceptions/helpers.tsx | 13 +- .../public/lists_plugin_deps.ts | 1 + .../routes/__mocks__/request_responses.ts | 34 +- .../routes/__mocks__/utils.ts | 34 +- .../routes/rules/validate.test.ts | 34 +- .../rules/get_export_all.test.ts | 32 +- .../rules/get_export_by_object_ids.test.ts | 64 +- .../lib/detection_engine/rules/types.ts | 9 +- .../lib/detection_engine/rules/utils.ts | 4 +- .../scripts/rules/patches/update_list.json | 27 +- .../rules/queries/lists/query_with_and.json | 35 - .../queries/lists/query_with_excluded.json | 23 - .../queries/lists/query_with_exists.json | 18 - .../rules/queries/lists/query_with_list.json | 54 - .../queries/lists/query_with_list_plugin.json | 24 - .../rules/queries/lists/query_with_match.json | 23 - .../queries/lists/query_with_match_all.json | 26 - .../rules/queries/lists/query_with_or.json | 32 - .../rules/queries/query_with_list.json | 10 + .../scripts/rules/updates/update_list.json | 30 +- .../signals/__mocks__/es_results.ts | 34 +- .../signals/build_bulk_body.test.ts | 133 +- .../signals/build_exceptions_query.test.ts | 1199 +++++++---------- .../signals/build_exceptions_query.ts | 162 +-- .../signals/build_rule.test.ts | 100 +- .../signals/filter_events_with_list.test.ts | 169 +-- .../signals/filter_events_with_list.ts | 118 +- .../signals/get_filter.test.ts | 117 +- .../detection_engine/signals/get_filter.ts | 6 +- .../signals/search_after_bulk_create.test.ts | 134 +- .../signals/search_after_bulk_create.ts | 4 +- .../signals/signal_rule_alert_type.test.ts | 14 +- .../signals/signal_rule_alert_type.ts | 35 +- .../detection_engine/signals/utils.test.ts | 113 +- .../lib/detection_engine/signals/utils.ts | 118 +- .../server/lib/detection_engine/types.ts | 4 +- 71 files changed, 2513 insertions(+), 2179 deletions(-) create mode 100644 x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts create mode 100644 x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json create mode 100644 x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_list.json diff --git a/x-pack/plugins/lists/README.md b/x-pack/plugins/lists/README.md index cdd7813792fc3..5c97107cf2282 100644 --- a/x-pack/plugins/lists/README.md +++ b/x-pack/plugins/lists/README.md @@ -157,12 +157,14 @@ And you can attach exception list items like so: { "field": "actingProcess.file.signer", "operator": "included", - "match": "Elastic, N.V." + "type": "match", + "value": "Elastic, N.V." }, { "field": "event.category", "operator": "included", - "match_any": [ + "type": "match_any", + "value": [ "process", "malware" ] diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts index 24cfe440bd7d8..185de02d555b7 100644 --- a/x-pack/plugins/lists/common/constants.mock.ts +++ b/x-pack/plugins/lists/common/constants.mock.ts @@ -46,10 +46,8 @@ export const EXISTS = 'exists'; export const NESTED = 'nested'; export const ENTRIES: EntriesArray = [ { - entries: [ - { field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' }, - ], - field: 'some.field', + entries: [{ field: 'nested.field', operator: 'included', type: 'match', value: 'some value' }], + field: 'some.parentField', type: 'nested', }, { field: 'some.not.nested.field', operator: 'included', type: 'match', value: 'some value' }, diff --git a/x-pack/plugins/lists/common/schemas/types/default_entries_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_entries_array.test.ts index 9e615528ba775..e7910be6bf4b5 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_entries_array.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_entries_array.test.ts @@ -17,7 +17,8 @@ import { getEntriesArrayMock, getEntryMatchMock, getEntryNestedMock } from './en // it checks against every item in that union. Since entries consist of 5 // different entry types, it returns 5 of these. To make more readable, // extracted here. -const returnedSchemaError = `"Array<({| field: string, operator: "excluded" | "included", type: "match", value: string |} | {| field: string, operator: "excluded" | "included", type: "match_any", value: DefaultStringArray |} | {| field: string, operator: "excluded" | "included", type: "list", value: DefaultStringArray |} | {| field: string, operator: "excluded" | "included", type: "exists" |} | {| entries: Array<({| field: string, operator: "excluded" | "included", type: "match", value: string |} | {| field: string, operator: "excluded" | "included", type: "match_any", value: DefaultStringArray |} | {| field: string, operator: "excluded" | "included", type: "list", value: DefaultStringArray |} | {| field: string, operator: "excluded" | "included", type: "exists" |})>, field: string, type: "nested" |})>"`; +const returnedSchemaError = + '"Array<({| field: string, operator: "excluded" | "included", type: "match", value: string |} | {| field: string, operator: "excluded" | "included", type: "match_any", value: DefaultStringArray |} | {| field: string, list: {| id: string, type: "ip" | "keyword" |}, operator: "excluded" | "included", type: "list" |} | {| field: string, operator: "excluded" | "included", type: "exists" |} | {| entries: Array<{| field: string, operator: "excluded" | "included", type: "match", value: string |}>, field: string, type: "nested" |})>"'; describe('default_entries_array', () => { test('it should validate an empty array', () => { diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts new file mode 100644 index 0000000000000..152f85233aa1a --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { DefaultNamespace } from './default_namespace'; + +describe('default_namespace', () => { + test('it should validate "single"', () => { + const payload = 'single'; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate "agnostic"', () => { + const payload = 'agnostic'; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it defaults to "single" if "undefined"', () => { + const payload = undefined; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('single'); + }); + + test('it defaults to "single" if "null"', () => { + const payload = null; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('single'); + }); + + test('it should NOT validate if not "single" or "agnostic"', () => { + const payload = 'something else'; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + `Invalid value "something else" supplied to "DefaultNamespace"`, + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts index c98cb8d2bba72..8f8f8d105b624 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -const namespaceType = t.keyof({ agnostic: null, single: null }); +export const namespaceType = t.keyof({ agnostic: null, single: null }); type NamespaceType = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts index 1926cb09db119..8af18c970c6ae 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.mock.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.mock.ts @@ -9,10 +9,12 @@ import { EXISTS, FIELD, LIST, + LIST_ID, MATCH, MATCH_ANY, NESTED, OPERATOR, + TYPE, } from '../../constants.mock'; import { @@ -40,9 +42,9 @@ export const getEntryMatchAnyMock = (): EntryMatchAny => ({ export const getEntryListMock = (): EntryList => ({ field: FIELD, + list: { id: LIST_ID, type: TYPE }, operator: OPERATOR, type: LIST, - value: [ENTRY_VALUE], }); export const getEntryExistsMock = (): EntryExists => ({ @@ -52,7 +54,7 @@ export const getEntryExistsMock = (): EntryExists => ({ }); export const getEntryNestedMock = (): EntryNested => ({ - entries: [getEntryMatchMock(), getEntryExistsMock()], + entries: [getEntryMatchMock(), getEntryMatchMock()], field: FIELD, type: NESTED, }); diff --git a/x-pack/plugins/lists/common/schemas/types/entries.test.ts b/x-pack/plugins/lists/common/schemas/types/entries.test.ts index a13d4c0347e45..01f82f12f2b2c 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.test.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.test.ts @@ -251,16 +251,16 @@ describe('Entries', () => { expect(message.schema).toEqual(payload); }); - test('it should not validate when "value" is not string array', () => { - const payload: Omit & { value: string } = { + test('it should not validate when "list" is not expected value', () => { + const payload: Omit & { list: string } = { ...getEntryListMock(), - value: 'someListId', + list: 'someListId', }; const decoded = entriesList.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "someListId" supplied to "value"', + 'Invalid value "someListId" supplied to "list"', ]); expect(message.schema).toEqual({}); }); @@ -338,6 +338,20 @@ describe('Entries', () => { expect(message.schema).toEqual({}); }); + test('it should NOT validate when "entries" contains an entry item that is not type "match"', () => { + const payload: Omit & { + entries: EntryMatchAny[]; + } = { ...getEntryNestedMock(), entries: [getEntryMatchAnyMock()] }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "match_any" supplied to "entries,type"', + 'Invalid value "["some host name"]" supplied to "entries,value"', + ]); + expect(message.schema).toEqual({}); + }); + test('it should strip out extra keys', () => { const payload: EntryNested & { extraKey?: string; diff --git a/x-pack/plugins/lists/common/schemas/types/entries.ts b/x-pack/plugins/lists/common/schemas/types/entries.ts index e3625dbe08334..c379f77b862c8 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; -import { operator } from '../common/schemas'; +import { operator, type } from '../common/schemas'; import { DefaultStringArray } from '../../siem_common_deps'; export const entriesMatch = t.exact( @@ -34,9 +34,9 @@ export type EntryMatchAny = t.TypeOf; export const entriesList = t.exact( t.type({ field: t.string, + list: t.exact(t.type({ id: t.string, type })), operator, type: t.keyof({ list: null }), - value: DefaultStringArray, }) ); export type EntryList = t.TypeOf; @@ -52,7 +52,7 @@ export type EntryExists = t.TypeOf; export const entriesNested = t.exact( t.type({ - entries: t.array(t.union([entriesMatch, entriesMatchAny, entriesList, entriesExists])), + entries: t.array(entriesMatch), field: t.string, type: t.keyof({ nested: null }), }) diff --git a/x-pack/plugins/lists/common/schemas/types/index.ts b/x-pack/plugins/lists/common/schemas/types/index.ts index 8e4b28b31d95c..97f2b0f59a5fd 100644 --- a/x-pack/plugins/lists/common/schemas/types/index.ts +++ b/x-pack/plugins/lists/common/schemas/types/index.ts @@ -5,5 +5,6 @@ */ export * from './default_comments_array'; export * from './default_entries_array'; +export * from './default_namespace'; export * from './comments'; export * from './entries'; diff --git a/x-pack/plugins/lists/server/index.ts b/x-pack/plugins/lists/server/index.ts index 33f58ba65d3c3..31f22108028a6 100644 --- a/x-pack/plugins/lists/server/index.ts +++ b/x-pack/plugins/lists/server/index.ts @@ -11,6 +11,7 @@ import { ListPlugin } from './plugin'; // exporting these since its required at top level in siem plugin export { ListClient } from './services/lists/list_client'; +export { ExceptionListClient } from './services/exception_lists/exception_list_client'; export { ListPluginSetup } from './types'; export const config = { schema: ConfigSchema }; diff --git a/x-pack/plugins/lists/server/saved_objects/exception_list.ts b/x-pack/plugins/lists/server/saved_objects/exception_list.ts index 10f9b1f4383f5..57bc63e6f7e35 100644 --- a/x-pack/plugins/lists/server/saved_objects/exception_list.ts +++ b/x-pack/plugins/lists/server/saved_objects/exception_list.ts @@ -105,6 +105,16 @@ export const exceptionListItemMapping: SavedObjectsType['mappings'] = { field: { type: 'keyword', }, + list: { + properties: { + id: { + type: 'keyword', + }, + type: { + type: 'keyword', + }, + }, + }, operator: { type: 'keyword', }, diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json new file mode 100644 index 0000000000000..e1dab72c1c7f6 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/exception_lists/new/exception_list_item_with_list.json @@ -0,0 +1,24 @@ +{ + "list_id": "endpoint_list", + "item_id": "endpoint_list_item_lg_val_list", + "_tags": ["endpoint", "process", "malware", "os:windows"], + "tags": ["user added string for a tag", "malware"], + "type": "simple", + "description": "This is a sample exception list item with a large value list included", + "name": "Sample Endpoint Exception List Item with large value list", + "comments": [], + "entries": [ + { + "field": "event.module", + "operator": "excluded", + "type": "match_any", + "value": ["zeek"] + }, + { + "field": "source.ip", + "operator": "excluded", + "type": "list", + "list": { "id": "list-ip", "type": "ip" } + } + ] +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json index 1516fa5057e50..1ece2268f3cf6 100644 --- a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json @@ -1,5 +1,5 @@ { "id": "hand_inserted_item_id", "list_id": "list-ip", - "value": "127.0.0.1" + "value": "10.4.2.140" } diff --git a/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts b/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.ts new file mode 100644 index 0000000000000..a8b177f587a48 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/lists_common_deps.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 { EntriesArray, namespaceType } from '../../../lists/common/schemas'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 0c7bcdefd360d..f6b732cd1f64e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -341,40 +341,3 @@ export type Note = t.TypeOf; export const noteOrUndefined = t.union([note, t.undefined]); export type NoteOrUndefined = t.TypeOf; - -// NOTE: Experimental list support not being shipped currently and behind a feature flag -// TODO: Remove this comment once we lists have passed testing and is ready for the release -export const list_field = t.string; -export const list_values_operator = t.keyof({ included: null, excluded: null }); -export const list_values_type = t.keyof({ match: null, match_all: null, list: null, exists: null }); -export const list_values = t.exact( - t.intersection([ - t.type({ - name: t.string, - }), - t.partial({ - id: t.string, - description: t.string, - created_at, - }), - ]) -); -export const list = t.exact( - t.intersection([ - t.type({ - field: t.string, - values_operator: list_values_operator, - values_type: list_values_type, - }), - t.partial({ values: t.array(list_values) }), - ]) -); -export const list_and = t.intersection([ - list, - t.partial({ - and: t.array(list), - }), -]); - -export const listAndOrUndefined = t.union([t.array(list_and), t.undefined]); -export type ListAndOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts index 3e7e7e5409c9c..43000f6d36f46 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts @@ -40,16 +40,19 @@ import { } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ -import { DefaultStringArray } from '../types/default_string_array'; -import { DefaultActionsArray } from '../types/default_actions_array'; -import { DefaultBooleanFalse } from '../types/default_boolean_false'; -import { DefaultFromString } from '../types/default_from_string'; -import { DefaultIntervalString } from '../types/default_interval_string'; -import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number'; -import { DefaultToString } from '../types/default_to_string'; -import { DefaultThreatArray } from '../types/default_threat_array'; -import { DefaultThrottleNull } from '../types/default_throttle_null'; -import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array'; +import { + DefaultStringArray, + DefaultActionsArray, + DefaultBooleanFalse, + DefaultFromString, + DefaultIntervalString, + DefaultMaxSignalsNumber, + DefaultToString, + DefaultThreatArray, + DefaultThrottleNull, + DefaultListArray, + ListArray, +} from '../types'; /** * Big differences between this schema and the createRulesSchema @@ -96,7 +99,7 @@ export const addPrepackagedRulesSchema = t.intersection([ throttle: DefaultThrottleNull, // defaults to "null" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode - exceptions_list: ListsDefaultArray, // defaults to empty array if not set during decode + exceptions_list: DefaultListArray, // defaults to empty array if not set during decode }) ), ]); @@ -130,5 +133,5 @@ export type AddPrepackagedRulesSchemaDecoded = Omit< to: To; threat: Threat; throttle: ThrottleOrNull; - exceptions_list: ListsDefaultArraySchema; + exceptions_list: ListArray; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts index f946b3ad3b39b..47a98166927b4 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts @@ -19,6 +19,7 @@ import { getAddPrepackagedRulesSchemaDecodedMock, } from './add_prepackaged_rules_schema.mock'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; +import { getListArrayMock } from '../types/lists.mock'; describe('add prepackaged rules schema', () => { test('empty objects do not validate', () => { @@ -1379,14 +1380,189 @@ describe('add prepackaged rules schema', () => { }); }); - // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin - describe.skip('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + describe('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, version, and exceptions_list] does validate', () => { + const payload: AddPrepackagedRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + version: 1, + exceptions_list: getListArrayMock(), + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: false, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + filters: [], + exceptions_list: [ + { + id: 'some_uuid', + namespace_type: 'single', + }, + { + id: 'some_uuid', + namespace_type: 'agnostic', + }, + ], + }; + expect(message.schema).toEqual(expected); + }); - test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, version, and empty exceptions_list] does validate', () => { + const payload: AddPrepackagedRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + version: 1, + note: '# some markdown', + exceptions_list: [], + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: false, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + filters: [], + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, version, and invalid exceptions_list] does NOT validate', () => { + const payload: Omit & { + exceptions_list: Array<{ id: string; namespace_type: string }>; + } = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + version: 1, + note: '# some markdown', + exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, version, and non-existent exceptions_list] does validate with empty exceptions_list', () => { + const payload: AddPrepackagedRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + version: 1, + note: '# some markdown', + }; + + const decoded = addPrepackagedRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: AddPrepackagedRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: false, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + exceptions_list: [], + filters: [], + }; + expect(message.schema).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts index a126b833ba461..1648044f5305a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts @@ -18,6 +18,7 @@ import { getCreateRulesSchemaDecodedMock, } from './create_rules_schema.mock'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; +import { getListArrayMock } from '../types/lists.mock'; describe('create rules schema', () => { test('empty objects do not validate', () => { @@ -1435,14 +1436,185 @@ describe('create rules schema', () => { ); }); - // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin - describe.skip('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + describe('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and exceptions_list] does validate', () => { + const payload: CreateRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: getListArrayMock(), + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + filters: [], + exceptions_list: [ + { + id: 'some_uuid', + namespace_type: 'single', + }, + { + id: 'some_uuid', + namespace_type: 'agnostic', + }, + ], + }; + expect(message.schema).toEqual(expected); + }); - test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { + const payload: CreateRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [], + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + filters: [], + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { + const payload: Omit & { + exceptions_list: Array<{ id: string; namespace_type: string }>; + } = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], + }; + + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { + const payload: CreateRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + }; + + const decoded = createRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: CreateRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + exceptions_list: [], + filters: [], + }; + expect(message.schema).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts index 4e60201b8030e..d623cff8f1fc3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts @@ -41,18 +41,21 @@ import { } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ -import { DefaultStringArray } from '../types/default_string_array'; -import { DefaultActionsArray } from '../types/default_actions_array'; -import { DefaultBooleanTrue } from '../types/default_boolean_true'; -import { DefaultFromString } from '../types/default_from_string'; -import { DefaultIntervalString } from '../types/default_interval_string'; -import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number'; -import { DefaultToString } from '../types/default_to_string'; -import { DefaultThreatArray } from '../types/default_threat_array'; -import { DefaultThrottleNull } from '../types/default_throttle_null'; -import { DefaultVersionNumber } from '../types/default_version_number'; -import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array'; -import { DefaultUuid } from '../types/default_uuid'; +import { + DefaultStringArray, + DefaultActionsArray, + DefaultBooleanTrue, + DefaultFromString, + DefaultIntervalString, + DefaultMaxSignalsNumber, + DefaultToString, + DefaultThreatArray, + DefaultThrottleNull, + DefaultVersionNumber, + DefaultListArray, + ListArray, + DefaultUuid, +} from '../types'; export const createRulesSchema = t.intersection([ t.exact( @@ -92,7 +95,7 @@ export const createRulesSchema = t.intersection([ references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode version: DefaultVersionNumber, // defaults to 1 if not set during decode - exceptions_list: ListsDefaultArray, // defaults to empty array if not set during decode + exceptions_list: DefaultListArray, // defaults to empty array if not set during decode }) ), ]); @@ -129,6 +132,6 @@ export type CreateRulesSchemaDecoded = Omit< threat: Threat; throttle: ThrottleOrNull; version: Version; - exceptions_list: ListsDefaultArraySchema; + exceptions_list: ListArray; rule_id: RuleId; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts index 9fe3e95a20621..12a13ab1a5ed1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts @@ -22,6 +22,7 @@ import { getImportRulesSchemaDecodedMock, } from './import_rules_schema.mock'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; +import { getListArrayMock } from '../types/lists.mock'; describe('import rules schema', () => { test('empty objects do not validate', () => { @@ -1569,14 +1570,188 @@ describe('import rules schema', () => { }); }); - // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin - describe.skip('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + describe('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and exceptions_list] does validate', () => { + const payload: ImportRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: getListArrayMock(), + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + filters: [], + immutable: false, + exceptions_list: [ + { + id: 'some_uuid', + namespace_type: 'single', + }, + { + id: 'some_uuid', + namespace_type: 'agnostic', + }, + ], + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { + const payload: ImportRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [], + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + immutable: false, + filters: [], + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { + const payload: Omit & { + exceptions_list: Array<{ id: string; namespace_type: string }>; + } = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], + }; - test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { + const payload: ImportRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + }; + + const decoded = importRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: ImportRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + version: 1, + immutable: false, + exceptions_list: [], + filters: [], + }; + expect(message.schema).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts index a2110263e8e51..7d79861aacf38 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts @@ -47,19 +47,22 @@ import { } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ -import { DefaultStringArray } from '../types/default_string_array'; -import { DefaultActionsArray } from '../types/default_actions_array'; -import { DefaultBooleanTrue } from '../types/default_boolean_true'; -import { DefaultFromString } from '../types/default_from_string'; -import { DefaultIntervalString } from '../types/default_interval_string'; -import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number'; -import { DefaultToString } from '../types/default_to_string'; -import { DefaultThreatArray } from '../types/default_threat_array'; -import { DefaultThrottleNull } from '../types/default_throttle_null'; -import { DefaultVersionNumber } from '../types/default_version_number'; -import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array'; -import { OnlyFalseAllowed } from '../types/only_false_allowed'; -import { DefaultStringBooleanFalse } from '../types/default_string_boolean_false'; +import { + DefaultStringArray, + DefaultActionsArray, + DefaultBooleanTrue, + DefaultFromString, + DefaultIntervalString, + DefaultMaxSignalsNumber, + DefaultToString, + DefaultThreatArray, + DefaultThrottleNull, + DefaultVersionNumber, + OnlyFalseAllowed, + DefaultStringBooleanFalse, + DefaultListArray, + ListArray, +} from '../types'; /** * Differences from this and the createRulesSchema are @@ -111,7 +114,7 @@ export const importRulesSchema = t.intersection([ references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode version: DefaultVersionNumber, // defaults to 1 if not set during decode - exceptions_list: ListsDefaultArray, // defaults to empty array if not set during decode + exceptions_list: DefaultListArray, // defaults to empty array if not set during decode created_at, // defaults "undefined" if not set during decode updated_at, // defaults "undefined" if not set during decode created_by, // defaults "undefined" if not set during decode @@ -153,7 +156,7 @@ export type ImportRulesSchemaDecoded = Omit< threat: Threat; throttle: ThrottleOrNull; version: Version; - exceptions_list: ListsDefaultArraySchema; + exceptions_list: ListArray; rule_id: RuleId; immutable: false; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts index 55363ffb18307..81a17df43daf6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.test.ts @@ -10,6 +10,7 @@ import { exactCheck } from '../../../exact_check'; import { pipe } from 'fp-ts/lib/pipeable'; import { foldLeftRight, getPaths } from '../../../test_utils'; import { left } from 'fp-ts/lib/Either'; +import { getListArrayMock } from '../types/lists.mock'; describe('patch_rules_schema', () => { test('made up values do not validate', () => { @@ -1139,14 +1140,156 @@ describe('patch_rules_schema', () => { expect(message.schema).toEqual({}); }); - // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin - describe.skip('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + describe('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, note, and exceptions_list] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + note: '# some documentation markdown', + exceptions_list: getListArrayMock(), + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + note: '# some documentation markdown', + exceptions_list: [ + { + id: 'some_uuid', + namespace_type: 'single', + }, + { + id: 'some_uuid', + namespace_type: 'agnostic', + }, + ], + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [], + }; + + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); + + test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { + const payload: Omit & { + exceptions_list: Array<{ id: string; namespace_type: string }>; + } = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', + 'Invalid value "[{"id":"uuid_here","namespace_type":"not a namespace type"}]" supplied to "exceptions_list"', + ]); + expect(message.schema).toEqual({}); + }); - test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { + const payload: PatchRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + const decoded = patchRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: PatchRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + filters: [], + risk_score: 50, + note: '# some markdown', + }; + expect(message.schema).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts index 605e0272bbb4c..29d5467071a3d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts @@ -37,10 +37,10 @@ import { references, to, language, - listAndOrUndefined, query, id, } from '../common/schemas'; +import { listArrayOrUndefined } from '../types/lists'; /* eslint-enable @typescript-eslint/camelcase */ /** @@ -80,7 +80,7 @@ export const patchRulesSchema = t.exact( references, note, version, - exceptions_list: listAndOrUndefined, + exceptions_list: listArrayOrUndefined, }) ); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts index 1ff38f1351f59..02f8e7bbeb59b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts @@ -18,6 +18,7 @@ import { getUpdateRulesSchemaDecodedMock, } from './update_rules_schema.mock'; import { DEFAULT_MAX_SIGNALS } from '../../../constants'; +import { getListArrayMock } from '../types/lists.mock'; describe('update rules schema', () => { test('empty objects do not validate', () => { @@ -1377,14 +1378,182 @@ describe('update rules schema', () => { }); }); - // TODO: The exception_list tests are skipped and empty until we re-integrate it from the lists plugin - describe.skip('exception_list', () => { - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and exceptions_list] does validate', () => {}); + describe('exception_list', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and exceptions_list] does validate', () => { + const payload: UpdateRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + filters: [], + note: '# some markdown', + exceptions_list: getListArrayMock(), + }; + + const decoded = updateRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: UpdateRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + filters: [], + exceptions_list: [ + { + id: 'some_uuid', + namespace_type: 'single', + }, + { + id: 'some_uuid', + namespace_type: 'agnostic', + }, + ], + }; + expect(message.schema).toEqual(expected); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => { + const payload: UpdateRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + filters: [], + note: '# some markdown', + exceptions_list: [], + }; + + const decoded = updateRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: UpdateRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + filters: [], + exceptions_list: [], + }; + expect(message.schema).toEqual(expected); + }); - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty exceptions_list] does validate', () => {}); + test('rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and invalid exceptions_list] does NOT validate', () => { + const payload: Omit & { + exceptions_list: Array<{ id: string; namespace_type: string }>; + } = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + filters: [], + note: '# some markdown', + exceptions_list: [{ id: 'uuid_here', namespace_type: 'not a namespace type' }], + }; + + const decoded = updateRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "not a namespace type" supplied to "exceptions_list,namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); - test('rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid exceptions_list] does NOT validate', () => {}); + test('[rule_id, description, from, to, index, name, severity, interval, type, filters, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => { + const payload: UpdateRulesSchema = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + filters: [], + note: '# some markdown', + }; - test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent exceptions_list] does validate with empty exceptions_list', () => {}); + const decoded = updateRulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected: UpdateRulesSchemaDecoded = { + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + references: [], + actions: [], + enabled: true, + false_positives: [], + max_signals: DEFAULT_MAX_SIGNALS, + tags: [], + threat: [], + throttle: null, + exceptions_list: [], + filters: [], + }; + expect(message.schema).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts index 504233f95986f..73078e617efc6 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts @@ -43,16 +43,19 @@ import { } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ -import { DefaultStringArray } from '../types/default_string_array'; -import { DefaultActionsArray } from '../types/default_actions_array'; -import { DefaultBooleanTrue } from '../types/default_boolean_true'; -import { DefaultFromString } from '../types/default_from_string'; -import { DefaultIntervalString } from '../types/default_interval_string'; -import { DefaultMaxSignalsNumber } from '../types/default_max_signals_number'; -import { DefaultToString } from '../types/default_to_string'; -import { DefaultThreatArray } from '../types/default_threat_array'; -import { DefaultThrottleNull } from '../types/default_throttle_null'; -import { ListsDefaultArray, ListsDefaultArraySchema } from '../types/lists_default_array'; +import { + DefaultStringArray, + DefaultActionsArray, + DefaultBooleanTrue, + DefaultFromString, + DefaultIntervalString, + DefaultMaxSignalsNumber, + DefaultToString, + DefaultThreatArray, + DefaultThrottleNull, + DefaultListArray, + ListArray, +} from '../types'; /** * This almost identical to the create_rules_schema except for a few details. @@ -100,7 +103,7 @@ export const updateRulesSchema = t.intersection([ references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode version, // defaults to "undefined" if not set during decode - exceptions_list: ListsDefaultArray, // defaults to empty array if not set during decode + exceptions_list: DefaultListArray, // defaults to empty array if not set during decode }) ), ]); @@ -135,6 +138,6 @@ export type UpdateRulesSchemaDecoded = Omit< to: To; threat: Threat; throttle: ThrottleOrNull; - exceptions_list: ListsDefaultArraySchema; + exceptions_list: ListArray; rule_id: RuleId; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index ecbf0321cdc67..e63a7ad981e12 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -3,6 +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 { getListArrayMock } from '../types/lists.mock'; import { RulesSchema } from './rules_schema'; @@ -64,38 +65,7 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem language: 'kuery', rule_id: 'query-rule-id', interval: '5m', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }); export const getRulesMlSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts index 90aef656db369..b3f9096b51483 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.test.ts @@ -22,6 +22,7 @@ import { exactCheck } from '../../../exact_check'; import { foldLeftRight, getPaths } from '../../../test_utils'; import { TypeAndTimelineOnly } from './type_timeline_only_schema'; import { getRulesSchemaMock, getRulesMlSchemaMock } from './rules_schema.mocks'; +import { ListArray } from '../types/lists'; export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; @@ -650,4 +651,47 @@ describe('rules_schema', () => { expect(fields.length).toEqual(2); }); }); + + describe('exceptions_list', () => { + test('it should validate an empty array for "exceptions_list"', () => { + const payload = getRulesSchemaMock(); + payload.exceptions_list = []; + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getRulesSchemaMock(); + expected.exceptions_list = []; + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it should NOT validate when "exceptions_list" is not expected type', () => { + const payload: Omit & { + exceptions_list?: string; + } = { ...getRulesSchemaMock(), exceptions_list: 'invalid_data' }; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid_data" supplied to "exceptions_list"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should default to empty array if "exceptions_list" is undefined ', () => { + const payload: Omit & { + exceptions_list?: ListArray; + } = getRulesSchemaMock(); + payload.exceptions_list = undefined; + + const decoded = rulesSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ ...payload, exceptions_list: [] }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index a7a31ec9e1b59..9803a80f57857 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -56,7 +56,7 @@ import { meta, note, } from '../common/schemas'; -import { ListsDefaultArray } from '../types/lists_default_array'; +import { DefaultListArray } from '../types/lists_default_array'; /** * This is the required fields for the rules schema response. Put all required properties on @@ -87,7 +87,7 @@ export const requiredRulesSchema = t.type({ updated_at, created_by, version, - exceptions_list: ListsDefaultArray, + exceptions_list: DefaultListArray, }); export type RequiredRulesSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts new file mode 100644 index 0000000000000..368dd4922eec4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './default_actions_array'; +export * from './default_boolean_false'; +export * from './default_boolean_true'; +export * from './default_empty_string'; +export * from './default_export_file_name'; +export * from './default_from_string'; +export * from './default_interval_string'; +export * from './default_language_string'; +export * from './default_max_signals_number'; +export * from './default_page'; +export * from './default_per_page'; +export * from './default_string_array'; +export * from './default_string_boolean_false'; +export * from './default_threat_array'; +export * from './default_throttle_null'; +export * from './default_to_string'; +export * from './default_uuid'; +export * from './default_version_number'; +export * from './iso_date_string'; +export * from './lists'; +export * from './lists_default_array'; +export * from './non_empty_string'; +export * from './only_false_allowed'; +export * from './positive_integer'; +export * from './positive_integer_greater_than_zero'; +export * from './references_default_array'; +export * from './risk_score'; +export * from './uuid'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts new file mode 100644 index 0000000000000..d76e2ac78f3d3 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { List, ListArray } from './lists'; + +export const getListMock = (): List => ({ + id: 'some_uuid', + namespace_type: 'single', +}); + +export const getListAgnosticMock = (): List => ({ + id: 'some_uuid', + namespace_type: 'agnostic', +}); + +export const getListArrayMock = (): ListArray => [getListMock(), getListAgnosticMock()]; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.test.ts new file mode 100644 index 0000000000000..657a4b479f164 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.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 { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { foldLeftRight, getPaths } from '../../../test_utils'; + +import { getListAgnosticMock, getListMock, getListArrayMock } from './lists.mock'; +import { + List, + ListArray, + ListArrayOrUndefined, + list, + listArray, + listArrayOrUndefined, +} from './lists'; + +describe('Lists', () => { + describe('list', () => { + test('it should validate a list', () => { + const payload = getListMock(); + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a list with "namespace_type" of"agnostic"', () => { + const payload = getListAgnosticMock(); + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a list without an "id"', () => { + const payload = getListMock(); + delete payload.id; + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a list without "namespace_type"', () => { + const payload = getListMock(); + delete payload.namespace_type; + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: List & { + extraKey?: string; + } = getListMock(); + payload.extraKey = 'some value'; + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getListMock()); + }); + }); + + describe('listArray', () => { + test('it should validate an array of lists', () => { + const payload = getListArrayMock(); + const decoded = listArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate when unexpected type found in array', () => { + const payload = ([1] as unknown) as ListArray; + const decoded = listArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| id: string, namespace_type: "agnostic" | "single" |}>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('listArrayOrUndefined', () => { + test('it should validate an array of lists', () => { + const payload = getListArrayMock(); + const decoded = listArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when undefined', () => { + const payload = undefined; + const decoded = listArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an item that is not of type "list" in array', () => { + const payload = ([1] as unknown) as ListArrayOrUndefined; + const decoded = listArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "(Array<{| id: string, namespace_type: "agnostic" | "single" |}> | undefined)"', + 'Invalid value "[1]" supplied to "(Array<{| id: string, namespace_type: "agnostic" | "single" |}> | undefined)"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts new file mode 100644 index 0000000000000..07be038ff3526 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { namespaceType } from '../../lists_common_deps'; + +export const list = t.exact( + t.type({ + id: t.string, + namespace_type: namespaceType, + }) +); + +export type List = t.TypeOf; +export const listArray = t.array(list); +export type ListArray = t.TypeOf; +export const listArrayOrUndefined = t.union([listArray, t.undefined]); +export type ListArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts index 9eb55c22756fa..2268e47bd1149 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.test.ts @@ -4,187 +4,60 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ListsDefaultArray } from './lists_default_array'; import { pipe } from 'fp-ts/lib/pipeable'; import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '../../../test_utils'; - -describe('lists_default_array', () => { - test('it should validate an empty array', () => { - const payload: string[] = []; - const decoded = ListsDefaultArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - test('it should validate an array of lists', () => { - const payload = [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ]; - const decoded = ListsDefaultArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); +import { foldLeftRight, getPaths } from '../../../test_utils'; - test('it should not validate an array of lists that includes a values_operator other than included or excluded', () => { - const payload = [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'exists', - }, - { - field: 'host.hostname', - values_operator: 'jibber jabber', - values_type: 'exists', - }, - ]; - const decoded = ListsDefaultArray.decode(payload); - const message = pipe(decoded, foldLeftRight); +import { DefaultListArray, DefaultListArrayC } from './lists_default_array'; +import { getListArrayMock } from './lists.mock'; - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "jibber jabber" supplied to "values_operator"', - ]); - expect(message.schema).toEqual({}); - }); - - // TODO - this scenario should never come up, as the values key is forbidden when values_type is "exists" in the incoming schema - need to find a good way to do this in io-ts - test('it will validate an array of lists that includes "values" when "values_type" is "exists"', () => { - const payload = [ - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'exists', - values: [ - { - name: '127.0.0.1', - }, - ], - }, - ]; - const decoded = ListsDefaultArray.decode(payload); +describe('lists_default_array', () => { + test('it should return a default array when null', () => { + const payload = null; + const decoded = DefaultListArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); + expect(message.schema).toEqual([]); }); - // TODO - this scenario should never come up, as the values key is required when values_type is "match" in the incoming schema - need to find a good way to do this in io-ts - test('it will validate an array of lists that does not include "values" when "values_type" is "match"', () => { - const payload = [ - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - }, - ]; - const decoded = ListsDefaultArray.decode(payload); + test('it should return a default array when undefined', () => { + const payload = undefined; + const decoded = DefaultListArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); + expect(message.schema).toEqual([]); }); - // TODO - this scenario should never come up, as the values key is required when values_type is "match_all" in the incoming schema - need to find a good way to do this in io-ts - test('it will validate an array of lists that does not include "values" when "values_type" is "match_all"', () => { - const payload = [ - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match_all', - }, - ]; - const decoded = ListsDefaultArray.decode(payload); + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = DefaultListArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(payload); }); - // TODO - this scenario should never come up, as the values key is required when values_type is "list" in the incoming schema - need to find a good way to do this in io-ts - test('it should not validate an array of lists that does not include "values" when "values_type" is "list"', () => { - const payload = [ - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'list', - }, - ]; - const decoded = ListsDefaultArray.decode(payload); + test('it should validate an array of lists', () => { + const payload = getListArrayMock(); + const decoded = DefaultListArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); expect(message.schema).toEqual(payload); }); - test('it should not validate an array with a number', () => { - const payload = [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - values: [ - { - name: '127.0.0.1', - }, - ], - }, - 5, - ]; - const decoded = ListsDefaultArray.decode(payload); + test('it should not validate an array of non accepted types', () => { + // Terrible casting for purpose of tests + const payload = ([1] as unknown) as DefaultListArrayC; + const decoded = DefaultListArray.decode(payload); const message = pipe(decoded, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "5" supplied to "listsWithDefaultArray"', - 'Invalid value "5" supplied to "listsWithDefaultArray"', + 'Invalid value "1" supplied to "DefaultListArray"', ]); expect(message.schema).toEqual({}); }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = ListsDefaultArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts index 7fe98cdc300ef..ac5666cad23a7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts @@ -7,28 +7,18 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; -import { - list_and as listAnd, - list_values as listValues, - list_values_operator as listOperator, -} from '../common/schemas'; +import { ListArray, list } from './lists'; -export type List = t.TypeOf; -export type ListValues = t.TypeOf; -export type ListOperator = t.TypeOf; +export type DefaultListArrayC = t.Type; /** - * Types the ListsDefaultArray as: - * - If null or undefined, then a default array will be set for the list + * Types the DefaultListArray as: + * - If null or undefined, then a default array of type list will be set */ -export const ListsDefaultArray = new t.Type( - 'listsWithDefaultArray', - t.array(listAnd).is, - (input, context): Either => - input == null ? t.success([]) : t.array(listAnd).validate(input, context), +export const DefaultListArray: DefaultListArrayC = new t.Type( + 'DefaultListArray', + t.array(list).is, + (input, context): Either => + input == null ? t.success([]) : t.array(list).validate(input, context), t.identity ); - -export type ListsDefaultArrayC = typeof ListsDefaultArray; - -export type ListsDefaultArraySchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 2239de3764326..244819080c93d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -215,7 +215,7 @@ describe('Exception helpers', () => { fieldName: 'host.name', isNested: false, operator: 'is in list', - value: ['some host name'], + value: 'some-list-id', }, { fieldName: 'host.name', @@ -238,8 +238,8 @@ describe('Exception helpers', () => { { fieldName: 'host.name.host.name', isNested: true, - operator: 'exists', - value: null, + operator: 'is', + value: 'some host name', }, ]; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index f8b9c39801ae5..164940db619f9 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -19,6 +19,7 @@ import { OperatorTypeEnum, entriesNested, entriesExists, + entriesList, } from '../../../lists_plugin_deps'; /** @@ -87,6 +88,16 @@ export const getFormattedEntries = (entries: EntriesArray): FormattedEntry[] => return formattedEntries.flat(); }; +export const getEntryValue = (entry: Entry): string | string[] | null => { + if (entriesList.is(entry)) { + return entry.list.id; + } else if (entriesExists.is(entry)) { + return null; + } else { + return entry.value; + } +}; + /** * Helper method for `getFormattedEntries` */ @@ -100,7 +111,7 @@ export const formatEntry = ({ item: Entry; }): FormattedEntry => { const operator = getExceptionOperatorSelect(item); - const value = !entriesExists.is(item) ? item.value : null; + const value = getEntryValue(item); return { fieldName: isNested ? `${parent}.${item.field}` : item.field, diff --git a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts index 22732c86bd9a9..575ff26330a46 100644 --- a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts +++ b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts @@ -27,4 +27,5 @@ export { OperatorTypeEnum, entriesNested, entriesExists, + entriesList, } from '../../lists/common/schemas'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 9928ce4807da9..581946f2300b4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -27,6 +27,7 @@ import { RuleNotificationAlertType } from '../../notifications/types'; import { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; import { SetSignalsStatusSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/set_signal_status_schema'; import { getCreateRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/create_rules_schema.mock'; +import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], @@ -390,38 +391,7 @@ export const getResult = (): RuleAlertType => ({ references: ['http://www.example.com', 'https://ww.example.com'], note: '# Investigative notes', version: 1, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptionsList: getListArrayMock(), }, createdAt: new Date('2019-12-13T16:40:33.400Z'), updatedAt: new Date('2019-12-13T16:40:33.400Z'), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 063c9dffd66dd..7b7d3fbdea0bf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -8,6 +8,7 @@ import { Readable } from 'stream'; import { HapiReadableStream } from '../../rules/types'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; +import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; /** * Given a string, builds a hapi stream as our @@ -76,38 +77,7 @@ export const getOutputRuleAlertForRest = (): Omit< ], }, ], - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), filters: [ { query: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index 1f5442e23d884..0065696712628 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -14,6 +14,7 @@ import { FindResult } from '../../../../../../alerts/server'; import { BulkError } from '../utils'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; import { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; +import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; export const ruleOutput: RulesSchema = { actions: [], @@ -68,38 +69,7 @@ export const ruleOutput: RulesSchema = { }, }, ], - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], meta: { someMeta: 'someField', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts index ee21c33540024..7d4bbfdced432 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts @@ -80,36 +80,8 @@ describe('getExportAll', () => { note: '# Investigative notes', version: 1, exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, + { id: 'some_uuid', namespace_type: 'single' }, + { id: 'some_uuid', namespace_type: 'agnostic' }, ], })}\n`, exportDetails: `${JSON.stringify({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index b00b7353a370f..043e563a4c8b5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -88,36 +88,8 @@ describe('get_export_by_object_ids', () => { note: '# Investigative notes', version: 1, exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, + { id: 'some_uuid', namespace_type: 'single' }, + { id: 'some_uuid', namespace_type: 'agnostic' }, ], })}\n`, exportDetails: `${JSON.stringify({ @@ -216,36 +188,8 @@ describe('get_export_by_object_ids', () => { note: '# Investigative notes', version: 1, exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, + { id: 'some_uuid', namespace_type: 'single' }, + { id: 'some_uuid', namespace_type: 'agnostic' }, ], }, ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 4b84057f6d795..fc95f0cfeb78e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -14,7 +14,6 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { ListsDefaultArraySchema } from '../../../../common/detection_engine/schemas/types/lists_default_array'; import { FalsePositives, From, @@ -62,7 +61,6 @@ import { ThreatOrUndefined, TypeOrUndefined, ReferencesOrUndefined, - ListAndOrUndefined, PerPageOrUndefined, PageOrUndefined, SortFieldOrUndefined, @@ -80,6 +78,7 @@ import { AlertsClient, PartialAlert } from '../../../../../alerts/server'; import { Alert, SanitizedAlert } from '../../../../../alerts/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { RuleTypeParams, PartialFilter } from '../types'; +import { ListArrayOrUndefined, ListArray } from '../../../../common/detection_engine/schemas/types'; export interface RuleAlertType extends Alert { params: RuleTypeParams; @@ -194,7 +193,7 @@ export interface CreateRulesOptions { references: References; note: NoteOrUndefined; version: Version; - exceptionsList: ListsDefaultArraySchema; + exceptionsList: ListArray; actions: RuleAlertAction[]; } @@ -230,7 +229,7 @@ export interface UpdateRulesOptions { references: References; note: NoteOrUndefined; version: VersionOrUndefined; - exceptionsList: ListsDefaultArraySchema; + exceptionsList: ListArray; actions: RuleAlertAction[]; } @@ -264,7 +263,7 @@ export interface PatchRulesOptions { references: ReferencesOrUndefined; note: NoteOrUndefined; version: VersionOrUndefined; - exceptionsList: ListAndOrUndefined; + exceptionsList: ListArrayOrUndefined; actions: RuleAlertAction[] | undefined; rule: SanitizedAlert | null; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index d40cb5d96669b..5c620a5df61f8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -31,9 +31,9 @@ import { ThreatOrUndefined, TypeOrUndefined, ReferencesOrUndefined, - ListAndOrUndefined, } from '../../../../common/detection_engine/schemas/common/schemas'; import { PartialFilter } from '../types'; +import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types'; export const calculateInterval = ( interval: string | undefined, @@ -74,7 +74,7 @@ export interface UpdateProperties { references: ReferencesOrUndefined; note: NoteOrUndefined; version: VersionOrUndefined; - exceptionsList: ListAndOrUndefined; + exceptionsList: ListArrayOrUndefined; anomalyThreshold: AnomalyThresholdOrUndefined; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/update_list.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/update_list.json index 8d831f3a961d8..6323597fc0946 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/update_list.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/patches/update_list.json @@ -2,31 +2,8 @@ "rule_id": "query-with-list", "exceptions_list": [ { - "field": "source.ip", - "values_operator": "excluded", - "values_type": "exists" - }, - { - "field": "host.name", - "values_operator": "included", - "values_type": "match", - "values": [ - { - "name": "rock01" - } - ], - "and": [ - { - "field": "host.id", - "values_operator": "included", - "values_type": "match_all", - "values": [ - { - "name": "123456" - } - ] - } - ] + "id": "some_updated_fake_id", + "namespace_type": "single" } ] } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json deleted file mode 100644 index 1575a712e2cba..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_and.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "List - and", - "description": "Query with a list that includes and. This rule should only produce signals when host.name exists and when both event.module is endgame and event.category is anything other than file", - "rule_id": "query-with-list-and", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "event.module", - "values_operator": "excluded", - "values_type": "match", - "values": [ - { - "name": "endgame" - } - ], - "and": [ - { - "field": "event.category", - "values_operator": "included", - "values_type": "match", - "values": [ - { - "name": "file" - } - ] - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json deleted file mode 100644 index 4e6d9403a276f..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_excluded.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "List - excluded", - "description": "Query with a list of values_operator excluded. This rule should only produce signals when host.name exists and event.module is suricata", - "rule_id": "query-with-list-excluded", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "event.module", - "values_operator": "excluded", - "values_type": "match", - "values": [ - { - "name": "suricata" - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json deleted file mode 100644 index 97beace37633f..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_exists.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "List - exists", - "description": "Query with a list that includes exists. This rule should only produce signals when host.name exists and event.action does not exist", - "rule_id": "query-with-list-exists", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "event.action", - "values_operator": "included", - "values_type": "exists" - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json deleted file mode 100644 index ad0585b5a2ec5..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "Query with a list", - "description": "Query with a list. This rule should only produce signals when either host.name exists and event.module is system and user.name is zeek or gdm OR when host.name exists and event.module is not endgame or zeek or system.", - "rule_id": "query-with-list", - "risk_score": 2, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "event.module", - "values_operator": "excluded", - "values_type": "match", - "values": [ - { - "name": "system" - } - ], - "and": [ - { - "field": "user.name", - "values_operator": "excluded", - "values_type": "match_all", - "values": [ - { - "name": "zeek" - }, - { - "name": "gdm" - } - ] - } - ] - }, - { - "field": "event.module", - "values_operator": "included", - "values_type": "match_all", - "values": [ - { - "name": "endgame" - }, - { - "name": "zeek" - }, - { - "name": "system" - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json deleted file mode 100644 index fa6fe6ac71117..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_list_plugin.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "Query with a list", - "description": "Query with a list only generate signals if source.ip is not in list", - "rule_id": "query-with-list", - "risk_score": 2, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "source.ip", - "values_operator": "excluded", - "values_type": "list", - "values": [ - { - "id": "ci-badguys.txt", - "name": "ip" - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json deleted file mode 100644 index 6e6880cc28f24..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "List - match", - "description": "Query with a list that includes match. This rule should only produce signals when host.name exists and event.module is not suricata", - "rule_id": "query-with-list-match", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "event.module", - "values_operator": "included", - "values_type": "match", - "values": [ - { - "name": "suricata" - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json deleted file mode 100644 index 44cc26ac3315e..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_match_all.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "List - match_all", - "description": "Query with a list that includes match_all. This rule should only produce signals when host.name exists and event.module is not suricata or auditd", - "rule_id": "query-with-list-match-all", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "language": "kuery", - "exceptions_list": [ - { - "field": "event.module", - "values_operator": "included", - "values_type": "match_all", - "values": [ - { - "name": "suricata" - }, - { - "name": "auditd" - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json deleted file mode 100644 index 9c4eda559d5bc..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/lists/query_with_or.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "List - or", - "description": "Query with a list that includes or. This rule should only produce signals when host.name exists and event.module is suricata OR when host.name exists and event.category is file", - "rule_id": "query-with-list-or", - "risk_score": 1, - "severity": "high", - "type": "query", - "query": "host.name: *", - "interval": "30s", - "exceptions_list": [ - { - "field": "event.module", - "values_operator": "excluded", - "values_type": "match", - "values": [ - { - "name": "suricata" - } - ] - }, - { - "field": "event.category", - "values_operator": "excluded", - "values_type": "match", - "values": [ - { - "name": "file" - } - ] - } - ] -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_list.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_list.json new file mode 100644 index 0000000000000..1cb4c144aa293 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_list.json @@ -0,0 +1,10 @@ +{ + "name": "Rule w exceptions", + "description": "Sample rule with exception list", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "host.name: *", + "interval": "30s", + "exceptions_list": [{ "id": "endpoint_list", "namespace_type": "single" }] +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/updates/update_list.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/updates/update_list.json index df22dff5c046e..f7359d586bd86 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/updates/update_list.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/updates/update_list.json @@ -6,33 +6,5 @@ "severity": "high", "type": "query", "query": "user.name: root or user.name: admin", - "exceptions_list": [ - { - "field": "source.ip", - "values_operator": "excluded", - "values_type": "exists" - }, - { - "field": "host.name", - "values_operator": "included", - "values_type": "match", - "values": [ - { - "name": "rock01" - } - ], - "and": [ - { - "field": "host.id", - "values_operator": "included", - "values_type": "match_all", - "values": [ - { - "name": "123456" - } - ] - } - ] - } - ] + "exceptions_list": [{ "id": "some_updated_fake_id", "namespace_type": "single" }] } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 101c998efa242..50f6e7d9e9c10 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -14,6 +14,7 @@ import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks import { RuleTypeParams } from '../../types'; import { IRuleStatusAttributes } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; export const sampleRuleAlertParams = ( maxSignals?: number | undefined, @@ -44,38 +45,7 @@ export const sampleRuleAlertParams = ( meta: undefined, threat: undefined, version: 1, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptionsList: getListArrayMock(), }); export const sampleDocNoSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index 80c2441193a0c..ad43932818836 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -12,6 +12,7 @@ import { } from './__mocks__/es_results'; import { buildBulkBody } from './build_bulk_body'; import { SignalHit } from './types'; +import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; describe('buildBulkBody', () => { beforeEach(() => { @@ -91,38 +92,7 @@ describe('buildBulkBody', () => { version: 1, created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }, }, }; @@ -218,38 +188,7 @@ describe('buildBulkBody', () => { updated_at: fakeSignalSourceHit.signal.rule?.updated_at, throttle: 'no_actions', threat: [], - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }, }, }; @@ -343,38 +282,7 @@ describe('buildBulkBody', () => { created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, throttle: 'no_actions', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }, }, }; @@ -461,38 +369,7 @@ describe('buildBulkBody', () => { updated_at: fakeSignalSourceHit.signal.rule?.updated_at, created_at: fakeSignalSourceHit.signal.rule?.created_at, throttle: 'no_actions', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts index 07adfde71c1a9..ce7cc50e81d67 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts @@ -6,16 +6,24 @@ import { buildQueryExceptions, - buildExceptions, + buildExceptionItemEntries, operatorBuilder, buildExists, buildMatch, - buildMatchAll, + buildMatchAny, evaluateValues, formatQuery, getLanguageBooleanOperator, + buildNested, } from './build_exceptions_query'; -import { List } from '../../../../common/detection_engine/schemas/types/lists_default_array'; +import { + EntriesArray, + EntryExists, + EntryMatch, + EntryMatchAny, + EntryNested, +} from '../../../../../lists/common/schemas'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; describe('build_exceptions_query', () => { describe('getLanguageBooleanOperator', () => { @@ -34,30 +42,30 @@ describe('build_exceptions_query', () => { describe('operatorBuilder', () => { describe('kuery', () => { - test('it returns "not " when operator is "excluded"', () => { - const operator = operatorBuilder({ operator: 'excluded', language: 'kuery' }); + test('it returns "not " when operator is "included"', () => { + const operator = operatorBuilder({ operator: 'included', language: 'kuery' }); - expect(operator).toEqual(' and '); + expect(operator).toEqual('not '); }); - test('it returns empty string when operator is "included"', () => { - const operator = operatorBuilder({ operator: 'included', language: 'kuery' }); + test('it returns empty string when operator is "excluded"', () => { + const operator = operatorBuilder({ operator: 'excluded', language: 'kuery' }); - expect(operator).toEqual(' and not '); + expect(operator).toEqual(''); }); }); describe('lucene', () => { - test('it returns "NOT " when operator is "excluded"', () => { - const operator = operatorBuilder({ operator: 'excluded', language: 'lucene' }); + test('it returns "NOT " when operator is "included"', () => { + const operator = operatorBuilder({ operator: 'included', language: 'lucene' }); - expect(operator).toEqual(' AND '); + expect(operator).toEqual('NOT '); }); - test('it returns empty string when operator is "included"', () => { - const operator = operatorBuilder({ operator: 'included', language: 'lucene' }); + test('it returns empty string when operator is "excluded"', () => { + const operator = operatorBuilder({ operator: 'excluded', language: 'lucene' }); - expect(operator).toEqual(' AND NOT '); + expect(operator).toEqual(''); }); }); }); @@ -65,161 +73,117 @@ describe('build_exceptions_query', () => { describe('buildExists', () => { describe('kuery', () => { test('it returns formatted wildcard string when operator is "excluded"', () => { - const query = buildExists({ operator: 'excluded', field: 'host.name', language: 'kuery' }); + const query = buildExists({ + item: { type: 'exists', operator: 'excluded', field: 'host.name' }, + language: 'kuery', + }); - expect(query).toEqual(' and host.name:*'); + expect(query).toEqual('host.name:*'); }); test('it returns formatted wildcard string when operator is "included"', () => { - const query = buildExists({ operator: 'included', field: 'host.name', language: 'kuery' }); + const query = buildExists({ + item: { type: 'exists', operator: 'included', field: 'host.name' }, + language: 'kuery', + }); - expect(query).toEqual(' and not host.name:*'); + expect(query).toEqual('not host.name:*'); }); }); describe('lucene', () => { test('it returns formatted wildcard string when operator is "excluded"', () => { - const query = buildExists({ operator: 'excluded', field: 'host.name', language: 'lucene' }); + const query = buildExists({ + item: { type: 'exists', operator: 'excluded', field: 'host.name' }, + language: 'lucene', + }); - expect(query).toEqual(' AND _exists_host.name'); + expect(query).toEqual('_exists_host.name'); }); test('it returns formatted wildcard string when operator is "included"', () => { - const query = buildExists({ operator: 'included', field: 'host.name', language: 'lucene' }); + const query = buildExists({ + item: { type: 'exists', operator: 'included', field: 'host.name' }, + language: 'lucene', + }); - expect(query).toEqual(' AND NOT _exists_host.name'); + expect(query).toEqual('NOT _exists_host.name'); }); }); }); describe('buildMatch', () => { describe('kuery', () => { - test('it returns empty string if no items in "values"', () => { - const query = buildMatch({ - operator: 'included', - field: 'host.name', - values: [], - language: 'kuery', - }); - - expect(query).toEqual(''); - }); - test('it returns formatted string when operator is "included"', () => { - const values = [ - { - name: 'suricata', - }, - ]; const query = buildMatch({ - operator: 'included', - field: 'host.name', - values, + item: { + type: 'match', + operator: 'included', + field: 'host.name', + value: 'suricata', + }, language: 'kuery', }); - expect(query).toEqual(' and not host.name:suricata'); + expect(query).toEqual('not host.name:suricata'); }); test('it returns formatted string when operator is "excluded"', () => { - const values = [ - { - name: 'suricata', - }, - ]; const query = buildMatch({ - operator: 'excluded', - field: 'host.name', - values, - language: 'kuery', - }); - - expect(query).toEqual(' and host.name:suricata'); - }); - - // TODO: need to clean up types and maybe restrict values to one if type is 'match' - test('it returns formatted string when "values" includes more than one item', () => { - const values = [ - { - name: 'suricata', - }, - { - name: 'auditd', + item: { + type: 'match', + operator: 'excluded', + field: 'host.name', + value: 'suricata', }, - ]; - const query = buildMatch({ - operator: 'included', - field: 'host.name', - values, language: 'kuery', }); - expect(query).toEqual(' and not host.name:suricata'); + expect(query).toEqual('host.name:suricata'); }); }); describe('lucene', () => { test('it returns formatted string when operator is "included"', () => { - const values = [ - { - name: 'suricata', - }, - ]; const query = buildMatch({ - operator: 'included', - field: 'host.name', - values, + item: { + type: 'match', + operator: 'included', + field: 'host.name', + value: 'suricata', + }, language: 'lucene', }); - expect(query).toEqual(' AND NOT host.name:suricata'); + expect(query).toEqual('NOT host.name:suricata'); }); test('it returns formatted string when operator is "excluded"', () => { - const values = [ - { - name: 'suricata', - }, - ]; const query = buildMatch({ - operator: 'excluded', - field: 'host.name', - values, - language: 'lucene', - }); - - expect(query).toEqual(' AND host.name:suricata'); - }); - - // TODO: need to clean up types and maybe restrict values to one if type is 'match' - test('it returns formatted string when "values" includes more than one item', () => { - const values = [ - { - name: 'suricata', - }, - { - name: 'auditd', + item: { + type: 'match', + operator: 'excluded', + field: 'host.name', + value: 'suricata', }, - ]; - const query = buildMatch({ - operator: 'included', - field: 'host.name', - values, language: 'lucene', }); - expect(query).toEqual(' AND NOT host.name:suricata'); + expect(query).toEqual('host.name:suricata'); }); }); }); - describe('buildMatchAll', () => { + describe('buildMatchAny', () => { describe('kuery', () => { test('it returns empty string if given an empty array for "values"', () => { - const exceptionSegment = buildMatchAll({ - operator: 'included', - field: 'host.name', - values: [], + const exceptionSegment = buildMatchAny({ + item: { + operator: 'included', + field: 'host.name', + value: [], + type: 'match_any', + }, language: 'kuery', }); @@ -227,113 +191,180 @@ describe('build_exceptions_query', () => { }); test('it returns formatted string when "values" includes only one item', () => { - const values = [ - { - name: 'suricata', + const exceptionSegment = buildMatchAny({ + item: { + operator: 'included', + field: 'host.name', + value: ['suricata'], + type: 'match_any', }, - ]; - const exceptionSegment = buildMatchAll({ - operator: 'included', - field: 'host.name', - values, language: 'kuery', }); - expect(exceptionSegment).toEqual(' and not host.name:suricata'); + expect(exceptionSegment).toEqual('not host.name:(suricata)'); }); test('it returns formatted string when operator is "included"', () => { - const values = [ - { - name: 'suricata', - }, - { - name: 'auditd', + const exceptionSegment = buildMatchAny({ + item: { + operator: 'included', + field: 'host.name', + value: ['suricata', 'auditd'], + type: 'match_any', }, - ]; - const exceptionSegment = buildMatchAll({ - operator: 'included', - field: 'host.name', - values, language: 'kuery', }); - expect(exceptionSegment).toEqual(' and not host.name:(suricata or auditd)'); + expect(exceptionSegment).toEqual('not host.name:(suricata or auditd)'); }); test('it returns formatted string when operator is "excluded"', () => { - const values = [ - { - name: 'suricata', - }, - { - name: 'auditd', + const exceptionSegment = buildMatchAny({ + item: { + operator: 'excluded', + field: 'host.name', + value: ['suricata', 'auditd'], + type: 'match_any', }, - ]; - const exceptionSegment = buildMatchAll({ - operator: 'excluded', - field: 'host.name', - values, language: 'kuery', }); - expect(exceptionSegment).toEqual(' and host.name:(suricata or auditd)'); + expect(exceptionSegment).toEqual('host.name:(suricata or auditd)'); }); }); describe('lucene', () => { test('it returns formatted string when operator is "included"', () => { - const values = [ - { - name: 'suricata', - }, - { - name: 'auditd', + const exceptionSegment = buildMatchAny({ + item: { + operator: 'included', + field: 'host.name', + value: ['suricata', 'auditd'], + type: 'match_any', }, - ]; - const exceptionSegment = buildMatchAll({ - operator: 'included', - field: 'host.name', - values, language: 'lucene', }); - expect(exceptionSegment).toEqual(' AND NOT host.name:(suricata OR auditd)'); + expect(exceptionSegment).toEqual('NOT host.name:(suricata OR auditd)'); }); test('it returns formatted string when operator is "excluded"', () => { - const values = [ - { - name: 'suricata', - }, - { - name: 'auditd', + const exceptionSegment = buildMatchAny({ + item: { + operator: 'excluded', + field: 'host.name', + value: ['suricata', 'auditd'], + type: 'match_any', }, - ]; - const exceptionSegment = buildMatchAll({ - operator: 'excluded', - field: 'host.name', - values, language: 'lucene', }); - expect(exceptionSegment).toEqual(' AND host.name:(suricata OR auditd)'); + expect(exceptionSegment).toEqual('host.name:(suricata OR auditd)'); }); test('it returns formatted string when "values" includes only one item', () => { - const values = [ - { - name: 'suricata', + const exceptionSegment = buildMatchAny({ + item: { + operator: 'included', + field: 'host.name', + value: ['suricata'], + type: 'match_any', }, - ]; - const exceptionSegment = buildMatchAll({ - operator: 'included', - field: 'host.name', - values, language: 'lucene', }); - expect(exceptionSegment).toEqual(' AND NOT host.name:suricata'); + expect(exceptionSegment).toEqual('NOT host.name:(suricata)'); + }); + }); + }); + + describe('buildNested', () => { + describe('kuery', () => { + test('it returns formatted query when one item in nested entry', () => { + const item: EntryNested = { + field: 'parent', + type: 'nested', + entries: [ + { + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', + }, + ], + }; + const result = buildNested({ item, language: 'kuery' }); + + expect(result).toEqual('parent:{ nestedField:value-3 }'); + }); + + test('it returns formatted query when multiple items in nested entry', () => { + const item: EntryNested = { + field: 'parent', + type: 'nested', + entries: [ + { + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', + }, + { + field: 'nestedFieldB', + operator: 'excluded', + type: 'match', + value: 'value-4', + }, + ], + }; + const result = buildNested({ item, language: 'kuery' }); + + expect(result).toEqual('parent:{ nestedField:value-3 and nestedFieldB:value-4 }'); + }); + }); + + // TODO: Does lucene support nested query syntax? + describe.skip('lucene', () => { + test('it returns formatted query when one item in nested entry', () => { + const item: EntryNested = { + field: 'parent', + type: 'nested', + entries: [ + { + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', + }, + ], + }; + const result = buildNested({ item, language: 'lucene' }); + + expect(result).toEqual('parent:{ nestedField:value-3 }'); + }); + + test('it returns formatted query when multiple items in nested entry', () => { + const item: EntryNested = { + field: 'parent', + type: 'nested', + entries: [ + { + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', + }, + { + field: 'nestedFieldB', + operator: 'excluded', + type: 'match', + value: 'value-4', + }, + ], + }; + const result = buildNested({ item, language: 'lucene' }); + + expect(result).toEqual('parent:{ nestedField:value-3 AND nestedFieldB:value-4 }'); }); }); }); @@ -341,110 +372,96 @@ describe('build_exceptions_query', () => { describe('evaluateValues', () => { describe('kuery', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { - const list: List = { - values_operator: 'included', - values_type: 'exists', + const list: EntryExists = { + operator: 'included', + type: 'exists', field: 'host.name', }; const result = evaluateValues({ - list, + item: list, language: 'kuery', }); - expect(result).toEqual(' and not host.name:*'); + expect(result).toEqual('not host.name:*'); }); test('it returns formatted string when "type" is "match"', () => { - const list: List = { - values_operator: 'included', - values_type: 'match', + const list: EntryMatch = { + operator: 'included', + type: 'match', field: 'host.name', - values: [{ name: 'suricata' }], + value: 'suricata', }; const result = evaluateValues({ - list, + item: list, language: 'kuery', }); - expect(result).toEqual(' and not host.name:suricata'); + expect(result).toEqual('not host.name:suricata'); }); - test('it returns formatted string when "type" is "match_all"', () => { - const list: List = { - values_operator: 'included', - values_type: 'match_all', + test('it returns formatted string when "type" is "match_any"', () => { + const list: EntryMatchAny = { + operator: 'included', + type: 'match_any', field: 'host.name', - values: [ - { - name: 'suricata', - }, - { - name: 'auditd', - }, - ], + value: ['suricata', 'auditd'], }; const result = evaluateValues({ - list, + item: list, language: 'kuery', }); - expect(result).toEqual(' and not host.name:(suricata or auditd)'); + expect(result).toEqual('not host.name:(suricata or auditd)'); }); }); describe('lucene', () => { describe('kuery', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { - const list: List = { - values_operator: 'included', - values_type: 'exists', + const list: EntryExists = { + operator: 'included', + type: 'exists', field: 'host.name', }; const result = evaluateValues({ - list, + item: list, language: 'lucene', }); - expect(result).toEqual(' AND NOT _exists_host.name'); + expect(result).toEqual('NOT _exists_host.name'); }); test('it returns formatted string when "type" is "match"', () => { - const list: List = { - values_operator: 'included', - values_type: 'match', + const list: EntryMatch = { + operator: 'included', + type: 'match', field: 'host.name', - values: [{ name: 'suricata' }], + value: 'suricata', }; const result = evaluateValues({ - list, + item: list, language: 'lucene', }); - expect(result).toEqual(' AND NOT host.name:suricata'); + expect(result).toEqual('NOT host.name:suricata'); }); - test('it returns formatted string when "type" is "match_all"', () => { - const list: List = { - values_operator: 'included', - values_type: 'match_all', + test('it returns formatted string when "type" is "match_any"', () => { + const list: EntryMatchAny = { + operator: 'included', + type: 'match_any', field: 'host.name', - values: [ - { - name: 'suricata', - }, - { - name: 'auditd', - }, - ], + value: ['suricata', 'auditd'], }; const result = evaluateValues({ - list, + item: list, language: 'lucene', }); - expect(result).toEqual(' AND NOT host.name:(suricata OR auditd)'); + expect(result).toEqual('NOT host.name:(suricata OR auditd)'); }); }); }); @@ -459,7 +476,7 @@ describe('build_exceptions_query', () => { test('it returns expected query string when single exception in array', () => { const formattedQuery = formatQuery({ - exceptions: [' and b:(value-1 or value-2) and not c:*'], + exceptions: ['b:(value-1 or value-2) and not c:*'], query: 'a:*', language: 'kuery', }); @@ -469,7 +486,7 @@ describe('build_exceptions_query', () => { test('it returns expected query string when multiple exceptions in array', () => { const formattedQuery = formatQuery({ - exceptions: [' and b:(value-1 or value-2) and not c:*', ' and not d:*'], + exceptions: ['b:(value-1 or value-2) and not c:*', 'not d:*'], query: 'a:*', language: 'kuery', }); @@ -480,149 +497,70 @@ describe('build_exceptions_query', () => { }); }); - describe('buildExceptions', () => { - test('it returns empty array if empty lists array passed in', () => { - const query = buildExceptions({ - query: 'a:*', + describe('buildExceptionItemEntries', () => { + test('it returns empty string if empty lists array passed in', () => { + const query = buildExceptionItemEntries({ language: 'kuery', lists: [], }); - expect(query).toEqual([]); + expect(query).toEqual(''); }); test('it returns expected query when more than one item in list', () => { // Equal to query && !(b && !c) -> (query AND NOT b) OR (query AND c) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const payload: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value-1', - }, - { - name: 'value-2', - }, - ], + operator: 'included', + type: 'match_any', + value: ['value-1', 'value-2'], }, { field: 'c', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'value-3', - }, - ], - }, - ]; - const query = buildExceptions({ - query: 'a:*', - language: 'kuery', - lists, - }); - const expectedQuery = [' and not b:(value-1 or value-2)', ' and c:value-3']; - - expect(query).toEqual(expectedQuery); - }); - - test('it returns expected query when list item includes nested "and" value', () => { - // Equal to query && !(b || !c) -> (query AND NOT b AND c) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ - { - field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value-1', - }, - { - name: 'value-2', - }, - ], - and: [ - { - field: 'c', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'value-3', - }, - ], - }, - ], + operator: 'excluded', + type: 'match', + value: 'value-3', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', - lists, + lists: payload, }); - const expectedQuery = [' and not b:(value-1 or value-2) and c:value-3']; + const expectedQuery = 'not b:(value-1 or value-2) and c:value-3'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list item includes nested "and" value of empty array', () => { + test('it returns expected query when list item includes nested value', () => { // Equal to query && !(b || !c) -> (query AND NOT b AND c) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value-1', - }, - { - name: 'value-2', - }, - ], - and: [], + operator: 'included', + type: 'match_any', + value: ['value-1', 'value-2'], }, - ]; - const query = buildExceptions({ - query: 'a:*', - language: 'kuery', - lists, - }); - const expectedQuery = [' and not b:(value-1 or value-2)']; - - expect(query).toEqual(expectedQuery); - }); - - test('it returns expected query when list item includes nested "and" value of null', () => { - // Equal to query && !(b || !c) -> (query AND NOT b AND c) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ { - field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ + field: 'parent', + type: 'nested', + entries: [ { - name: 'value-1', - }, - { - name: 'value-2', + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', }, ], - and: undefined, }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and not b:(value-1 or value-2)']; + const expectedQuery = 'not b:(value-1 or value-2) and parent:{ nestedField:value-3 }'; expect(query).toEqual(expectedQuery); }); @@ -630,130 +568,112 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items and nested "and" values', () => { // Equal to query && !((b || !c) && d) -> (query AND NOT b AND c) OR (query AND NOT d) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value-1', - }, - { - name: 'value-2', - }, - ], - and: [ + operator: 'included', + type: 'match_any', + value: ['value-1', 'value-2'], + }, + { + field: 'parent', + type: 'nested', + entries: [ { - field: 'c', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'value-3', - }, - ], + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', }, ], }, { field: 'd', - values_operator: 'included', - values_type: 'exists', + operator: 'included', + type: 'exists', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and not b:(value-1 or value-2) and c:value-3', ' and not d:*']; - + const expectedQuery = + 'not b:(value-1 or value-2) and parent:{ nestedField:value-3 } and not d:*'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when language is "lucene"', () => { // Equal to query && !((b || !c) && !d) -> (query AND NOT b AND c) OR (query AND d) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value-1', - }, - { - name: 'value-2', - }, - ], - and: [ + operator: 'included', + type: 'match_any', + value: ['value-1', 'value-2'], + }, + { + field: 'parent', + type: 'nested', + entries: [ { - field: 'c', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'value-3', - }, - ], + field: 'nestedField', + operator: 'excluded', + type: 'match', + value: 'value-3', }, ], }, { field: 'e', - values_operator: 'excluded', - values_type: 'exists', + operator: 'excluded', + type: 'exists', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'lucene', lists, }); - const expectedQuery = [' AND NOT b:(value-1 OR value-2) AND c:value-3', ' AND _exists_e']; - + const expectedQuery = + 'NOT b:(value-1 OR value-2) AND parent:{ nestedField:value-3 } AND _exists_e'; expect(query).toEqual(expectedQuery); }); describe('exists', () => { - test('it returns expected query when list includes single list item with values_operator of "included"', () => { + test('it returns expected query when list includes single list item with operator of "included"', () => { // Equal to query && !(b) -> (query AND NOT b) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'exists', + operator: 'included', + type: 'exists', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and not b:*']; + const expectedQuery = 'not b:*'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list includes single list item with values_operator of "excluded"', () => { + test('it returns expected query when list includes single list item with operator of "excluded"', () => { // Equal to query && !(!b) -> (query AND b) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'excluded', - values_type: 'exists', + operator: 'excluded', + type: 'exists', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and b:*']; + const expectedQuery = 'b:*'; expect(query).toEqual(expectedQuery); }); @@ -761,26 +681,30 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes list item with "and" values', () => { // Equal to query && !(!b || !c) -> (query AND b AND c) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'excluded', - values_type: 'exists', - and: [ + operator: 'excluded', + type: 'exists', + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'excluded', - values_type: 'exists', + operator: 'excluded', + type: 'match', + value: 'value-1', }, ], }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and b:* and c:*']; + const expectedQuery = 'b:* and parent:{ c:value-1 }'; expect(query).toEqual(expectedQuery); }); @@ -788,88 +712,83 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { // Equal to query && !((b || !c || d) && e) -> (query AND NOT b AND c AND NOT d) OR (query AND NOT e) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'exists', - and: [ + operator: 'included', + type: 'exists', + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'excluded', - values_type: 'exists', + operator: 'excluded', + type: 'match', + value: 'value-1', }, { field: 'd', - values_operator: 'included', - values_type: 'exists', + operator: 'included', + type: 'match', + value: 'value-2', }, ], }, { field: 'e', - values_operator: 'included', - values_type: 'exists', + operator: 'included', + type: 'exists', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and not b:* and c:* and not d:*', ' and not e:*']; + const expectedQuery = 'not b:* and parent:{ c:value-1 and d:value-2 } and not e:*'; expect(query).toEqual(expectedQuery); }); }); describe('match', () => { - test('it returns expected query when list includes single list item with values_operator of "included"', () => { + test('it returns expected query when list includes single list item with operator of "included"', () => { // Equal to query && !(b) -> (query AND NOT b) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match', - values: [ - { - name: 'value', - }, - ], + operator: 'included', + type: 'match', + value: 'value', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and not b:value']; + const expectedQuery = 'not b:value'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list includes single list item with values_operator of "excluded"', () => { + test('it returns expected query when list includes single list item with operator of "excluded"', () => { // Equal to query && !(!b) -> (query AND b) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'value', - }, - ], + operator: 'excluded', + type: 'match', + value: 'value', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and b:value']; + const expectedQuery = 'b:value'; expect(query).toEqual(expectedQuery); }); @@ -877,36 +796,31 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes list item with "and" values', () => { // Equal to query && !(!b || !c) -> (query AND b AND c) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'value', - }, - ], - and: [ + operator: 'excluded', + type: 'match', + value: 'value', + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'valueC', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueC', }, ], }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and b:value and c:valueC']; + const expectedQuery = 'b:value and parent:{ c:valueC }'; expect(query).toEqual(expectedQuery); }); @@ -914,160 +828,117 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { // Equal to query && !((b || !c || d) && e) -> (query AND NOT b AND c AND NOT d) OR (query AND NOT e) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match', - values: [ - { - name: 'value', - }, - ], - and: [ + operator: 'included', + type: 'match', + value: 'value', + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'valueC', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueC', }, { field: 'd', - values_operator: 'included', - values_type: 'match', - values: [ - { - name: 'valueC', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueC', }, ], }, { field: 'e', - values_operator: 'included', - values_type: 'match', - values: [ - { - name: 'valueC', - }, - ], + operator: 'included', + type: 'match', + value: 'valueC', }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [ - ' and not b:value and c:valueC and not d:valueC', - ' and not e:valueC', - ]; + const expectedQuery = 'not b:value and parent:{ c:valueC and d:valueC } and not e:valueC'; expect(query).toEqual(expectedQuery); }); }); - describe('match_all', () => { - test('it returns expected query when list includes single list item with values_operator of "included"', () => { + describe('match_any', () => { + test('it returns expected query when list includes single list item with operator of "included"', () => { // Equal to query && !(b) -> (query AND NOT b) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value', - }, - { - name: 'value-1', - }, - ], + operator: 'included', + type: 'match_any', + value: ['value', 'value-1'], }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and not b:(value or value-1)']; + const expectedQuery = 'not b:(value or value-1)'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list includes single list item with values_operator of "excluded"', () => { + test('it returns expected query when list includes single list item with operator of "excluded"', () => { // Equal to query && !(!b) -> (query AND b) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'excluded', - values_type: 'match_all', - values: [ - { - name: 'value', - }, - { - name: 'value-1', - }, - ], + operator: 'excluded', + type: 'match_any', + value: ['value', 'value-1'], }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and b:(value or value-1)']; + const expectedQuery = 'b:(value or value-1)'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list includes list item with "and" values', () => { + test('it returns expected query when list includes list item with nested values', () => { // Equal to query && !(!b || c) -> (query AND b AND NOT c) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'excluded', - values_type: 'match_all', - values: [ - { - name: 'value', - }, - { - name: 'value-1', - }, - ], - and: [ + operator: 'excluded', + type: 'match_any', + value: ['value', 'value-1'], + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueC', - }, - { - name: 'value-2', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueC', }, ], }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [' and b:(value or value-1) and not c:(valueC or value-2)']; + const expectedQuery = 'b:(value or value-1) and parent:{ c:valueC }'; expect(query).toEqual(expectedQuery); }); @@ -1075,71 +946,25 @@ describe('build_exceptions_query', () => { test('it returns expected query when list includes multiple items', () => { // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const lists: EntriesArray = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value', - }, - { - name: 'value-1', - }, - ], - and: [ - { - field: 'c', - values_operator: 'excluded', - values_type: 'match_all', - values: [ - { - name: 'valueC', - }, - { - name: 'value-2', - }, - ], - }, - { - field: 'd', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueD', - }, - { - name: 'value-3', - }, - ], - }, - ], + operator: 'included', + type: 'match_any', + value: ['value', 'value-1'], }, { field: 'e', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueE', - }, - { - name: 'value-4', - }, - ], + operator: 'included', + type: 'match_any', + value: ['valueE', 'value-4'], }, ]; - const query = buildExceptions({ - query: 'a:*', + const query = buildExceptionItemEntries({ language: 'kuery', lists, }); - const expectedQuery = [ - ' and not b:(value or value-1) and c:(valueC or value-2) and not d:(valueD or value-3)', - ' and not e:(valueE or value-4)', - ]; + const expectedQuery = 'not b:(value or value-1) and not e:(valueE or value-4)'; expect(query).toEqual(expectedQuery); }); @@ -1157,65 +982,47 @@ describe('build_exceptions_query', () => { test('it returns expected query when lists exist and language is "kuery"', () => { // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const payload = getExceptionListItemSchemaMock(); + const payload2 = getExceptionListItemSchemaMock(); + payload2.entries = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value', - }, - { - name: 'value-1', - }, - ], - and: [ + operator: 'included', + type: 'match_any', + value: ['value', 'value-1'], + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'excluded', - values_type: 'match_all', - values: [ - { - name: 'valueC', - }, - { - name: 'value-2', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueC', }, { field: 'd', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueD', - }, - { - name: 'value-3', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueD', }, ], }, { field: 'e', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueE', - }, - { - name: 'value-4', - }, - ], + operator: 'included', + type: 'match_any', + value: ['valueE', 'value-4'], }, ]; - const query = buildQueryExceptions({ query: 'a:*', language: 'kuery', lists }); + const query = buildQueryExceptions({ + query: 'a:*', + language: 'kuery', + lists: [payload, payload2], + }); const expectedQuery = - '(a:* and not b:(value or value-1) and c:(valueC or value-2) and not d:(valueD or value-3)) or (a:* and not e:(valueE or value-4))'; + '(a:* and some.parentField:{ nested.field:some value } and not some.not.nested.field:some value) or (a:* and not b:(value or value-1) and parent:{ c:valueC and d:valueD } and not e:(valueE or value-4))'; expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); }); @@ -1223,65 +1030,47 @@ describe('build_exceptions_query', () => { test('it returns expected query when lists exist and language is "lucene"', () => { // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) // https://www.dcode.fr/boolean-expressions-calculator - const lists: List[] = [ + const payload = getExceptionListItemSchemaMock(); + const payload2 = getExceptionListItemSchemaMock(); + payload2.entries = [ { field: 'b', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'value', - }, - { - name: 'value-1', - }, - ], - and: [ + operator: 'included', + type: 'match_any', + value: ['value', 'value-1'], + }, + { + field: 'parent', + type: 'nested', + entries: [ { field: 'c', - values_operator: 'excluded', - values_type: 'match_all', - values: [ - { - name: 'valueC', - }, - { - name: 'value-2', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueC', }, { field: 'd', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueD', - }, - { - name: 'value-3', - }, - ], + operator: 'excluded', + type: 'match', + value: 'valueD', }, ], }, { field: 'e', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: 'valueE', - }, - { - name: 'value-4', - }, - ], + operator: 'included', + type: 'match_any', + value: ['valueE', 'value-4'], }, ]; - const query = buildQueryExceptions({ query: 'a:*', language: 'lucene', lists }); + const query = buildQueryExceptions({ + query: 'a:*', + language: 'lucene', + lists: [payload, payload2], + }); const expectedQuery = - '(a:* AND NOT b:(value OR value-1) AND c:(valueC OR value-2) AND NOT d:(valueD OR value-3)) OR (a:* AND NOT e:(valueE OR value-4))'; + '(a:* AND some.parentField:{ nested.field:some value } AND NOT some.not.nested.field:some value) OR (a:* AND NOT b:(value OR value-1) AND parent:{ c:valueC AND d:valueD } AND NOT e:(valueE OR value-4))'; expect(query).toEqual([{ query: expectedQuery, language: 'lucene' }]); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts index 233b20792299b..ba0d9dec7d1b0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts @@ -3,17 +3,23 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - ListAndOrUndefined, - Language, - Query, -} from '../../../../common/detection_engine/schemas/common/schemas'; -import { - ListOperator, - ListValues, - List, -} from '../../../../common/detection_engine/schemas/types/lists_default_array'; +import { Language, Query } from '../../../../common/detection_engine/schemas/common/schemas'; import { Query as DataQuery } from '../../../../../../../src/plugins/data/server'; +import { + Entry, + ExceptionListItemSchema, + EntryMatch, + EntryMatchAny, + EntryNested, + EntryExists, + EntriesArray, + Operator, + entriesMatchAny, + entriesExists, + entriesMatch, + entriesNested, + entriesList, +} from '../../../../../lists/common/schemas'; type Operators = 'and' | 'or' | 'not'; type LuceneOperators = 'AND' | 'OR' | 'NOT'; @@ -41,37 +47,30 @@ export const operatorBuilder = ({ operator, language, }: { - operator: ListOperator; + operator: Operator; language: Language; }): string => { - const and = getLanguageBooleanOperator({ - language, - value: 'and', - }); - const or = getLanguageBooleanOperator({ + const not = getLanguageBooleanOperator({ language, value: 'not', }); switch (operator) { - case 'excluded': - return ` ${and} `; case 'included': - return ` ${and} ${or} `; + return `${not} `; default: return ''; } }; export const buildExists = ({ - operator, - field, + item, language, }: { - operator: ListOperator; - field: string; + item: EntryExists; language: Language; }): string => { + const { operator, field } = item; const exceptionOperator = operatorBuilder({ operator, language }); switch (language) { @@ -85,64 +84,70 @@ export const buildExists = ({ }; export const buildMatch = ({ - operator, - field, - values, + item, language, }: { - operator: ListOperator; - field: string; - values: ListValues[]; + item: EntryMatch; language: Language; }): string => { - if (values.length > 0) { - const exceptionOperator = operatorBuilder({ operator, language }); - const [exception] = values; + const { value, operator, field } = item; + const exceptionOperator = operatorBuilder({ operator, language }); - return `${exceptionOperator}${field}:${exception.name}`; - } else { - return ''; - } + return `${exceptionOperator}${field}:${value}`; }; -export const buildMatchAll = ({ - operator, - field, - values, +export const buildMatchAny = ({ + item, language, }: { - operator: ListOperator; - field: string; - values: ListValues[]; + item: EntryMatchAny; language: Language; }): string => { - switch (values.length) { + const { value, operator, field } = item; + + switch (value.length) { case 0: return ''; - case 1: - return buildMatch({ operator, field, values, language }); default: const or = getLanguageBooleanOperator({ language, value: 'or' }); const exceptionOperator = operatorBuilder({ operator, language }); - const matchAllValues = values.map((value) => { - return value.name; - }); + const matchAnyValues = value.map((v) => v); - return `${exceptionOperator}${field}:(${matchAllValues.join(` ${or} `)})`; + return `${exceptionOperator}${field}:(${matchAnyValues.join(` ${or} `)})`; } }; -export const evaluateValues = ({ list, language }: { list: List; language: Language }): string => { - const { values_operator: operator, values_type: type, field, values } = list; - switch (type) { - case 'exists': - return buildExists({ operator, field, language }); - case 'match': - return buildMatch({ operator, field, values: values ?? [], language }); - case 'match_all': - return buildMatchAll({ operator, field, values: values ?? [], language }); - default: - return ''; +export const buildNested = ({ + item, + language, +}: { + item: EntryNested; + language: Language; +}): string => { + const { field, entries } = item; + const and = getLanguageBooleanOperator({ language, value: 'and' }); + const values = entries.map((entry) => `${entry.field}:${entry.value}`); + + return `${field}:{ ${values.join(` ${and} `)} }`; +}; + +export const evaluateValues = ({ + item, + language, +}: { + item: Entry | EntryNested; + language: Language; +}): string => { + if (entriesExists.is(item)) { + return buildExists({ item, language }); + } else if (entriesMatch.is(item)) { + return buildMatch({ item, language }); + } else if (entriesMatchAny.is(item)) { + return buildMatchAny({ item, language }); + } else if (entriesNested.is(item)) { + return buildNested({ item, language }); + } else { + return ''; } }; @@ -157,8 +162,9 @@ export const formatQuery = ({ }): string => { if (exceptions.length > 0) { const or = getLanguageBooleanOperator({ language, value: 'or' }); + const and = getLanguageBooleanOperator({ language, value: 'and' }); const formattedExceptions = exceptions.map((exception) => { - return `(${query}${exception})`; + return `(${query} ${and} ${exception})`; }); return formattedExceptions.join(` ${or} `); @@ -167,23 +173,22 @@ export const formatQuery = ({ } }; -export const buildExceptions = ({ - query, +export const buildExceptionItemEntries = ({ lists, language, }: { - query: string; - lists: List[]; + lists: EntriesArray; language: Language; -}): string[] => { - return lists.reduce((accum, listItem) => { - const { and, ...exceptionDetails } = { ...listItem }; - const andExceptionsSegments = and ? buildExceptions({ query, lists: and, language }) : []; - const exceptionSegment = evaluateValues({ list: exceptionDetails, language }); - const exception = [...exceptionSegment, ...andExceptionsSegments]; - - return [...accum, exception.join('')]; - }, []); +}): string => { + const and = getLanguageBooleanOperator({ language, value: 'and' }); + const exceptionItem = lists + .filter((t) => !entriesList.is(t)) + .reduce((accum, listItem) => { + const exceptionSegment = evaluateValues({ item: listItem, language }); + return [...accum, exceptionSegment]; + }, []); + + return exceptionItem.join(` ${and} `); }; export const buildQueryExceptions = ({ @@ -193,12 +198,13 @@ export const buildQueryExceptions = ({ }: { query: Query; language: Language; - lists: ListAndOrUndefined; + lists: ExceptionListItemSchema[] | undefined; }): DataQuery[] => { if (lists && lists !== null) { - const exceptions = buildExceptions({ lists, language, query }); + const exceptions = lists.map((exceptionItem) => + buildExceptionItemEntries({ lists: exceptionItem.entries, language }) + ); const formattedQuery = formatQuery({ exceptions, language, query }); - return [ { query: formattedQuery, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts index eb87976a6fbab..9aef5a370b86a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts @@ -7,6 +7,7 @@ import { buildRule } from './build_rule'; import { sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; +import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; describe('buildRule', () => { beforeEach(() => { @@ -80,38 +81,7 @@ describe('buildRule', () => { query: 'host.name: Braden', }, ], - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), version: 1, }; expect(rule).toEqual(expected); @@ -164,38 +134,7 @@ describe('buildRule', () => { updated_at: rule.updated_at, created_at: rule.created_at, throttle: 'no_actions', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }; expect(rule).toEqual(expected); }); @@ -247,38 +186,7 @@ describe('buildRule', () => { updated_at: rule.updated_at, created_at: rule.created_at, throttle: 'no_actions', - exceptions_list: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'exists', - }, - { - field: 'host.name', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'rock01', - }, - ], - and: [ - { - field: 'host.id', - values_operator: 'included', - values_type: 'match_all', - values: [ - { - name: '123', - }, - { - name: '678', - }, - ], - }, - ], - }, - ], + exceptions_list: getListArrayMock(), }; expect(rule).toEqual(expected); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts index 4e9eb8587484f..bb56926390af9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts @@ -8,6 +8,7 @@ import uuid from 'uuid'; import { filterEventsAgainstList } from './filter_events_with_list'; import { mockLogger, repeatedSearchResultsWithSortId } from './__mocks__/es_results'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; import { listMock } from '../../../../../lists/server/mocks'; @@ -36,92 +37,42 @@ describe('filterEventsAgainstList', () => { expect(res.hits.hits.length).toEqual(4); }); - it('should throw an error if malformed exception list present', async () => { - let message = ''; - try { - await filterEventsAgainstList({ - logger: mockLogger, - listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'excluded', - values_type: 'list', - values: undefined, + describe('operator_type is included', () => { + it('should respond with same list if no items match value list', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', }, - ], - eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ - '1.1.1.1', - '2.2.2.2', - '3.3.3.3', - '7.7.7.7', - ]), - }); - } catch (exc) { - message = exc.message; - } - expect(message).toEqual( - 'Failed to query lists index. Reason: Malformed exception list provided' - ); - }); + }, + ]; - it('should throw an error if unsupported exception type', async () => { - let message = ''; - try { - await filterEventsAgainstList({ - logger: mockLogger, - listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'excluded', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'unsupportedListPluginType', - }, - ], - }, - ], - eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ - '1.1.1.1', - '2.2.2.2', - '3.3.3.3', - '7.7.7.7', - ]), - }); - } catch (exc) { - message = exc.message; - } - expect(message).toEqual( - 'Failed to query lists index. Reason: Unsupported list type used, please use one of ip,keyword' - ); - }); - - describe('operator_type is includes', () => { - it('should respond with same list if no items match value list', async () => { const res = await filterEventsAgainstList({ logger: mockLogger, listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)), }); expect(res.hits.hits.length).toEqual(4); }); it('should respond with less items in the list if some values match', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; listClient.getListItemByValues = jest.fn(({ value }) => Promise.resolve( value.slice(0, 2).map((item) => ({ @@ -133,19 +84,7 @@ describe('filterEventsAgainstList', () => { const res = await filterEventsAgainstList({ logger: mockLogger, listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ '1.1.1.1', '2.2.2.2', @@ -162,27 +101,39 @@ describe('filterEventsAgainstList', () => { }); describe('operator type is excluded', () => { it('should respond with empty list if no items match value list', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'excluded', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; const res = await filterEventsAgainstList({ logger: mockLogger, listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'excluded', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3)), }); expect(res.hits.hits.length).toEqual(0); }); it('should respond with less items in the list if some values match', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'excluded', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; listClient.getListItemByValues = jest.fn(({ value }) => Promise.resolve( value.slice(0, 2).map((item) => ({ @@ -194,19 +145,7 @@ describe('filterEventsAgainstList', () => { const res = await filterEventsAgainstList({ logger: mockLogger, listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'excluded', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], eventSearchResult: repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ '1.1.1.1', '2.2.2.2', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts index 48b120d1b5806..1a2f648eb8562 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts @@ -6,15 +6,17 @@ import { get } from 'lodash/fp'; import { Logger } from 'src/core/server'; -import { ListAndOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; -import { List } from '../../../../common/detection_engine/schemas/types/lists_default_array'; -import { type } from '../../../../../lists/common/schemas/common'; import { ListClient } from '../../../../../lists/server'; import { SignalSearchResponse, SearchTypes } from './types'; +import { + entriesList, + EntryList, + ExceptionListItemSchema, +} from '../../../../../lists/common/schemas'; interface FilterEventsAgainstList { listClient: ListClient; - exceptionsList: ListAndOrUndefined; + exceptionsList: ExceptionListItemSchema[]; logger: Logger; eventSearchResult: SignalSearchResponse; } @@ -34,63 +36,63 @@ export const filterEventsAgainstList = async ({ const isStringableType = (val: SearchTypes) => ['string', 'number', 'boolean'].includes(typeof val); // grab the signals with values found in the given exception lists. - const filteredHitsPromises = exceptionsList - .filter((exceptionItem: List) => exceptionItem.values_type === 'list') - .map(async (exceptionItem: List) => { - if (exceptionItem.values == null || exceptionItem.values.length === 0) { - throw new Error('Malformed exception list provided'); - } - if (!type.is(exceptionItem.values[0].name)) { - throw new Error( - `Unsupported list type used, please use one of ${Object.keys(type.keys).join()}` - ); - } - if (!exceptionItem.values[0].id) { - throw new Error(`Missing list id for exception on field ${exceptionItem.field}`); - } - // acquire the list values we are checking for. - const valuesOfGivenType = eventSearchResult.hits.hits.reduce((acc, searchResultItem) => { - const valueField = get(exceptionItem.field, searchResultItem._source); - if (valueField != null && isStringableType(valueField)) { - acc.add(valueField.toString()); - } - return acc; - }, new Set()); + const filteredHitsPromises = exceptionsList.map( + async (exceptionItem: ExceptionListItemSchema) => { + const { entries } = exceptionItem; - // matched will contain any list items that matched with the - // values passed in from the Set. - const matchedListItems = await listClient.getListItemByValues({ - listId: exceptionItem.values[0].id, - type: exceptionItem.values[0].name, - value: [...valuesOfGivenType], - }); + const filteredHitsEntries = entries + .filter((t): t is EntryList => entriesList.is(t)) + .map(async (entry) => { + // acquire the list values we are checking for. + const valuesOfGivenType = eventSearchResult.hits.hits.reduce( + (acc, searchResultItem) => { + const valueField = get(entry.field, searchResultItem._source); + if (valueField != null && isStringableType(valueField)) { + acc.add(valueField.toString()); + } + return acc; + }, + new Set() + ); - // create a set of list values that were a hit - easier to work with - const matchedListItemsSet = new Set( - matchedListItems.map((item) => item.value) - ); + // matched will contain any list items that matched with the + // values passed in from the Set. + const matchedListItems = await listClient.getListItemByValues({ + listId: entry.list.id, + type: entry.list.type, + value: [...valuesOfGivenType], + }); - // do a single search after with these values. - // painless script to do nested query in elasticsearch - // filter out the search results that match with the values found in the list. - const operator = exceptionItem.values_operator; - const filteredEvents = eventSearchResult.hits.hits.filter((item) => { - const eventItem = get(exceptionItem.field, item._source); - if (operator === 'included') { - if (eventItem != null) { - return !matchedListItemsSet.has(eventItem); - } - } else if (operator === 'excluded') { - if (eventItem != null) { - return matchedListItemsSet.has(eventItem); - } - } - return false; - }); - const diff = eventSearchResult.hits.hits.length - filteredEvents.length; - logger.debug(`Lists filtered out ${diff} events`); - return filteredEvents; - }); + // create a set of list values that were a hit - easier to work with + const matchedListItemsSet = new Set( + matchedListItems.map((item) => item.value) + ); + + // do a single search after with these values. + // painless script to do nested query in elasticsearch + // filter out the search results that match with the values found in the list. + const operator = entry.operator; + const filteredEvents = eventSearchResult.hits.hits.filter((item) => { + const eventItem = get(entry.field, item._source); + if (operator === 'included') { + if (eventItem != null) { + return !matchedListItemsSet.has(eventItem); + } + } else if (operator === 'excluded') { + if (eventItem != null) { + return matchedListItemsSet.has(eventItem); + } + } + return false; + }); + const diff = eventSearchResult.hits.hits.length - filteredEvents.length; + logger.debug(`Lists filtered out ${diff} events`); + return filteredEvents; + }); + + return (await Promise.all(filteredHitsEntries)).flat(); + } + ); const filteredHits = await Promise.all(filteredHitsPromises); const toReturn: SignalSearchResponse = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts index 61cd9cfedd94f..9b3a446bc666d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts @@ -7,6 +7,7 @@ import { getQueryFilter, getFilter } from './get_filter'; import { PartialFilter } from '../types'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; describe('get_filter', () => { let servicesMock: AlertServicesMock; @@ -381,18 +382,7 @@ describe('get_filter', () => { 'kuery', [], ['auditbeat-*'], - [ - { - field: 'event.module', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'suricata', - }, - ], - }, - ] + [getExceptionListItemSchemaMock()] ); expect(esQuery).toEqual({ bool: { @@ -414,11 +404,39 @@ describe('get_filter', () => { }, { bool: { - minimum_should_match: 1, - should: [ + filter: [ { - match: { - 'event.module': 'suricata', + nested: { + path: 'some.parentField', + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'some.parentField.nested.field': 'some value', + }, + }, + ], + }, + }, + score_mode: 'none', + }, + }, + { + bool: { + must_not: { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'some.not.nested.field': 'some value', + }, + }, + ], + }, + }, }, }, ], @@ -450,7 +468,7 @@ describe('get_filter', () => { }); test('it should work when lists has value undefined', () => { - const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], undefined); + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], []); expect(esQuery).toEqual({ bool: { filter: [ @@ -529,7 +547,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], - lists: undefined, + lists: [], }); expect(filter).toEqual({ bool: { @@ -564,7 +582,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], - lists: undefined, + lists: [], }) ).rejects.toThrow('query, filters, and index parameter should be defined'); }); @@ -579,7 +597,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], - lists: undefined, + lists: [], }) ).rejects.toThrow('query, filters, and index parameter should be defined'); }); @@ -594,7 +612,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: undefined, - lists: undefined, + lists: [], }) ).rejects.toThrow('query, filters, and index parameter should be defined'); }); @@ -608,7 +626,7 @@ describe('get_filter', () => { savedId: 'some-id', services: servicesMock, index: ['auditbeat-*'], - lists: undefined, + lists: [], }); expect(filter).toEqual({ bool: { @@ -632,7 +650,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], - lists: undefined, + lists: [], }) ).rejects.toThrow('savedId parameter should be defined'); }); @@ -647,7 +665,7 @@ describe('get_filter', () => { savedId: 'some-id', services: servicesMock, index: undefined, - lists: undefined, + lists: [], }) ).rejects.toThrow('savedId parameter should be defined'); }); @@ -662,7 +680,7 @@ describe('get_filter', () => { savedId: 'some-id', services: servicesMock, index: undefined, - lists: undefined, + lists: [], }) ).rejects.toThrow('Unsupported Rule of type "machine_learning" supplied to getFilter'); }); @@ -812,18 +830,7 @@ describe('get_filter', () => { savedId: undefined, services: servicesMock, index: ['auditbeat-*'], - lists: [ - { - field: 'event.module', - values_operator: 'excluded', - values_type: 'match', - values: [ - { - name: 'suricata', - }, - ], - }, - ], + lists: [getExceptionListItemSchemaMock()], }); expect(filter).toEqual({ bool: { @@ -845,11 +852,39 @@ describe('get_filter', () => { }, { bool: { - minimum_should_match: 1, - should: [ + filter: [ { - match: { - 'event.module': 'suricata', + nested: { + path: 'some.parentField', + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'some.parentField.nested.field': 'some value', + }, + }, + ], + }, + }, + score_mode: 'none', + }, + }, + { + bool: { + must_not: { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'some.not.nested.field': 'some value', + }, + }, + ], + }, + }, }, }, ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index 3e9f79c67d8ca..50ce01aaa6f74 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -10,11 +10,11 @@ import { Type, SavedIdOrUndefined, IndexOrUndefined, - ListAndOrUndefined, Language, Index, Query, } from '../../../../common/detection_engine/schemas/common/schemas'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { AlertServices } from '../../../../../alerts/server'; import { assertUnreachable } from '../../../utils/build_query'; import { @@ -33,7 +33,7 @@ export const getQueryFilter = ( language: Language, filters: PartialFilter[], index: Index, - lists: ListAndOrUndefined + lists: ExceptionListItemSchema[] ) => { const indexPattern = { fields: [], @@ -64,7 +64,7 @@ interface GetFilterArgs { savedId: SavedIdOrUndefined; services: AlertServices; index: IndexOrUndefined; - lists: ListAndOrUndefined; + lists: ExceptionListItemSchema[]; } interface QueryAttributes { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 163ed76d0c6c3..1923f43c47b92 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -17,6 +17,7 @@ import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mock import uuid from 'uuid'; import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; import { listMock } from '../../../../../lists/server/mocks'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; describe('searchAfterAndBulkCreate', () => { let mockService: AlertServicesMock; @@ -94,22 +95,23 @@ describe('searchAfterAndBulkCreate', () => { }, ], }); + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], services: mockService, logger: mockLogger, id: sampleRuleGuid, @@ -168,22 +170,22 @@ describe('searchAfterAndBulkCreate', () => { }, ], }); + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], services: mockService, logger: mockLogger, id: sampleRuleGuid, @@ -254,7 +256,7 @@ describe('searchAfterAndBulkCreate', () => { const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ ruleParams: sampleParams, listClient, - exceptionsList: undefined, + exceptionsList: [], services: mockService, logger: mockLogger, id: sampleRuleGuid, @@ -281,25 +283,25 @@ describe('searchAfterAndBulkCreate', () => { }); test('if unsuccessful first bulk create', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; const sampleParams = sampleRuleAlertParams(10); mockService.callCluster .mockResolvedValueOnce(repeatedSearchResultsWithSortId(4, 1, someGuids.slice(0, 3))) .mockRejectedValue(new Error('bulk failed')); // Added this recently const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -327,6 +329,18 @@ describe('searchAfterAndBulkCreate', () => { }); test('should return success with 0 total hits', async () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockResolvedValueOnce(sampleEmptyDocSearchResults()); listClient.getListItemByValues = jest.fn(({ value }) => @@ -339,19 +353,7 @@ describe('searchAfterAndBulkCreate', () => { ); const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], ruleParams: sampleParams, services: mockService, logger: mockLogger, @@ -405,21 +407,21 @@ describe('searchAfterAndBulkCreate', () => { })) ) ); + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ listClient, - exceptionsList: [ - { - field: 'source.ip', - values_operator: 'included', - values_type: 'list', - values: [ - { - id: 'ci-badguys.txt', - name: 'ip', - }, - ], - }, - ], + exceptionsList: [exceptionItem], ruleParams: sampleParams, services: mockService, logger: mockLogger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 65679dc23e64f..7475257121552 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ListAndOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; import { AlertServices } from '../../../../../alerts/server'; import { ListClient } from '../../../../../lists/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; @@ -14,12 +13,13 @@ import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; import { SignalSearchResponse } from './types'; import { filterEventsAgainstList } from './filter_events_with_list'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; interface SearchAfterAndBulkCreateParams { ruleParams: RuleTypeParams; services: AlertServices; listClient: ListClient | undefined; // TODO: undefined is for temporary development, remove before merged - exceptionsList: ListAndOrUndefined; + exceptionsList: ExceptionListItemSchema[]; logger: Logger; id: string; inputIndexPattern: string[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 23c2d6068c09c..5832b4075a40b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -10,7 +10,7 @@ import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { ruleStatusServiceFactory } from './rule_status_service'; -import { getGapBetweenRuns } from './utils'; +import { getGapBetweenRuns, getListsClient, getExceptions, sortExceptionItems } from './utils'; import { RuleExecutorOptions } from './types'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; @@ -18,6 +18,9 @@ import { RuleAlertType } from '../rules/types'; import { findMlSignals } from './find_ml_signals'; import { bulkCreateMlSignals } from './bulk_create_ml_signals'; import { listMock } from '../../../../../lists/server/mocks'; +import { getListClientMock } from '../../../../../lists/server/services/lists/list_client.mock'; +import { getExceptionListClientMock } from '../../../../../lists/server/services/exception_lists/exception_list_client.mock'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); @@ -84,6 +87,15 @@ describe('rules_notification_alert_type', () => { }; (ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService); (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(0)); + (getListsClient as jest.Mock).mockReturnValue({ + listClient: getListClientMock(), + exceptionsClient: getExceptionListClientMock(), + }); + (getExceptions as jest.Mock).mockReturnValue([getExceptionListItemSchemaMock()]); + (sortExceptionItems as jest.Mock).mockReturnValue({ + exceptionsWithoutValueLists: [getExceptionListItemSchemaMock()], + exceptionsWithValueLists: [], + }); (searchAfterAndBulkCreate as jest.Mock).mockClear(); (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({ success: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 728bd66b7d65c..1bf27dc6e26b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -15,9 +15,6 @@ import { } from '../../../../common/constants'; import { isJobStarted, isMlRule } from '../../../../common/machine_learning/helpers'; import { SetupPlugins } from '../../../plugin'; - -import { ListClient } from '../../../../../lists/server'; - import { getInputIndex } from './get_input_output_index'; import { searchAfterAndBulkCreate, @@ -25,7 +22,7 @@ import { } from './search_after_bulk_create'; import { getFilter } from './get_filter'; import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types'; -import { getGapBetweenRuns, parseScheduleDates } from './utils'; +import { getGapBetweenRuns, parseScheduleDates, getListsClient, getExceptions } from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; import { findMlSignals } from './find_ml_signals'; @@ -38,7 +35,6 @@ import { ruleStatusServiceFactory } from './rule_status_service'; import { buildRuleMessageFactory } from './rule_messages'; import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects_client'; import { getNotificationResultsLink } from '../notifications/utils'; -import { hasListsFeature } from '../feature_flags'; export const signalRulesAlertType = ({ logger, @@ -140,6 +136,18 @@ export const signalRulesAlertType = ({ await ruleStatusService.error(gapMessage, { gap: gapString }); } try { + const { listClient, exceptionsClient } = await getListsClient({ + services, + updatedByUser, + spaceId, + lists, + savedObjectClient: services.savedObjectsClient, + }); + const exceptionItems = await getExceptions({ + client: exceptionsClient, + lists: exceptionsList, + }); + if (isMlRule(type)) { if (ml == null) { throw new Error('ML plugin unavailable during rule execution'); @@ -214,18 +222,6 @@ export const signalRulesAlertType = ({ result.bulkCreateTimes.push(bulkCreateDuration); } } else { - let listClient: ListClient | undefined; - if (hasListsFeature()) { - if (lists == null) { - throw new Error('lists plugin unavailable during rule execution'); - } - listClient = await lists.getListClient( - services.callCluster, - spaceId, - updatedByUser ?? 'elastic' - ); - } - const inputIndex = await getInputIndex(services, version, index); const esFilter = await getFilter({ type, @@ -235,13 +231,12 @@ export const signalRulesAlertType = ({ savedId, services, index: inputIndex, - // temporary filter out list type - lists: exceptionsList?.filter((item) => item.values_type !== 'list'), + lists: exceptionItems ?? [], }); result = await searchAfterAndBulkCreate({ listClient, - exceptionsList, + exceptionsList: exceptionItems ?? [], ruleParams: params, services, logger, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index f74694df613ce..24c2d24ee972e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -7,6 +7,12 @@ import moment from 'moment'; import sinon from 'sinon'; +import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; +import { listMock } from '../../../../../lists/server/mocks'; +import { EntriesArray } from '../../../../common/detection_engine/lists_common_deps'; + +import * as featureFlags from '../feature_flags'; + import { generateId, parseInterval, @@ -14,10 +20,10 @@ import { getDriftTolerance, getGapBetweenRuns, errorAggregator, + getListsClient, + hasLargeValueList, } from './utils'; - import { BulkResponseErrorAggregation } from './types'; - import { sampleBulkResponse, sampleEmptyBulkResponse, @@ -529,4 +535,107 @@ describe('utils', () => { expect(aggregated).toEqual(expected); }); }); + + describe('#getListsClient', () => { + let alertServices: AlertServicesMock; + + beforeEach(() => { + alertServices = alertsMock.createAlertServices(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it successfully returns list and exceptions list client', async () => { + jest.spyOn(featureFlags, 'hasListsFeature').mockReturnValue(true); + + const { listClient, exceptionsClient } = await getListsClient({ + services: alertServices, + savedObjectClient: alertServices.savedObjectsClient, + updatedByUser: 'some_user', + spaceId: '', + lists: listMock.createSetup(), + }); + + expect(listClient).toBeDefined(); + expect(exceptionsClient).toBeDefined(); + }); + + test('it returns list and exceptions client of "undefined" if lists feature flag is off', async () => { + jest.spyOn(featureFlags, 'hasListsFeature').mockReturnValue(false); + + const listsClient = await getListsClient({ + services: alertServices, + savedObjectClient: alertServices.savedObjectsClient, + updatedByUser: 'some_user', + spaceId: '', + lists: listMock.createSetup(), + }); + + expect(listsClient).toEqual({ listClient: undefined, exceptionsClient: undefined }); + }); + + test('it throws if "lists" is undefined', async () => { + jest.spyOn(featureFlags, 'hasListsFeature').mockReturnValue(true); + + await expect(() => + getListsClient({ + services: alertServices, + savedObjectClient: alertServices.savedObjectsClient, + updatedByUser: 'some_user', + spaceId: '', + lists: undefined, + }) + ).rejects.toThrowError('lists plugin unavailable during rule execution'); + }); + }); + + describe('#hasLargeValueList', () => { + test('it returns false if empty array', () => { + const hasLists = hasLargeValueList([]); + + expect(hasLists).toBeFalsy(); + }); + + test('it returns true if item of type EntryList exists', () => { + const entries: EntriesArray = [ + { + field: 'actingProcess.file.signer', + type: 'list', + operator: 'included', + list: { id: 'some id', type: 'ip' }, + }, + { + field: 'file.signature.signer', + type: 'match', + operator: 'excluded', + value: 'Global Signer', + }, + ]; + const hasLists = hasLargeValueList(entries); + + expect(hasLists).toBeTruthy(); + }); + + test('it returns false if item of type EntryList does not exist', () => { + const entries: EntriesArray = [ + { + field: 'actingProcess.file.signer', + type: 'match', + operator: 'included', + value: 'Elastic, N.V.', + }, + { + field: 'file.signature.signer', + type: 'match', + operator: 'excluded', + value: 'Global Signer', + }, + ]; + const hasLists = hasLargeValueList(entries); + + expect(hasLists).toBeFalsy(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index f0ca08b73fac6..e431e65fad623 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -7,9 +7,125 @@ import { createHash } from 'crypto'; import moment from 'moment'; import dateMath from '@elastic/datemath'; -import { parseDuration } from '../../../../../alerts/server'; +import { SavedObjectsClientContract } from '../../../../../../../src/core/server'; +import { AlertServices, parseDuration } from '../../../../../alerts/server'; +import { ExceptionListClient, ListClient, ListPluginSetup } from '../../../../../lists/server'; +import { EntriesArray, ExceptionListItemSchema } from '../../../../../lists/common/schemas'; +import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; +import { hasListsFeature } from '../feature_flags'; import { BulkResponse, BulkResponseErrorAggregation } from './types'; +interface SortExceptionsReturn { + exceptionsWithValueLists: ExceptionListItemSchema[]; + exceptionsWithoutValueLists: ExceptionListItemSchema[]; +} + +export const getListsClient = async ({ + lists, + spaceId, + updatedByUser, + services, + savedObjectClient, +}: { + lists: ListPluginSetup | undefined; + spaceId: string; + updatedByUser: string | null; + services: AlertServices; + savedObjectClient: SavedObjectsClientContract; +}): Promise<{ + listClient: ListClient | undefined; + exceptionsClient: ExceptionListClient | undefined; +}> => { + // TODO Remove check once feature is no longer behind flag + if (hasListsFeature()) { + if (lists == null) { + throw new Error('lists plugin unavailable during rule execution'); + } + + const listClient = await lists.getListClient( + services.callCluster, + spaceId, + updatedByUser ?? 'elastic' + ); + const exceptionsClient = await lists.getExceptionListClient( + savedObjectClient, + updatedByUser ?? 'elastic' + ); + + return { listClient, exceptionsClient }; + } else { + return { listClient: undefined, exceptionsClient: undefined }; + } +}; + +export const hasLargeValueList = (entries: EntriesArray): boolean => { + const found = entries.filter(({ type }) => type === 'list'); + return found.length > 0; +}; + +export const getExceptions = async ({ + client, + lists, +}: { + client: ExceptionListClient | undefined; + lists: ListArrayOrUndefined; +}): Promise => { + // TODO Remove check once feature is no longer behind flag + if (hasListsFeature()) { + if (client == null) { + throw new Error('lists plugin unavailable during rule execution'); + } + + if (lists != null) { + try { + // Gather all exception items of all exception lists linked to rule + const exceptions = await Promise.all( + lists + .map(async (list) => { + const { id, namespace_type: namespaceType } = list; + const items = await client.findExceptionListItem({ + listId: id, + namespaceType, + page: 1, + perPage: 5000, + filter: undefined, + sortOrder: undefined, + sortField: undefined, + }); + return items != null ? items.data : []; + }) + .flat() + ); + return exceptions.flat(); + } catch { + return []; + } + } + } +}; + +export const sortExceptionItems = (exceptions: ExceptionListItemSchema[]): SortExceptionsReturn => { + return exceptions.reduce( + (acc, exception) => { + const { entries } = exception; + const { exceptionsWithValueLists, exceptionsWithoutValueLists } = acc; + + if (hasLargeValueList(entries)) { + return { + exceptionsWithValueLists: [...exceptionsWithValueLists, { ...exception }], + exceptionsWithoutValueLists, + }; + } else { + return { + exceptionsWithValueLists, + exceptionsWithoutValueLists: [...exceptionsWithoutValueLists, { ...exception }], + }; + } + }, + { exceptionsWithValueLists: [], exceptionsWithoutValueLists: [] } + ); +}; + export const generateId = ( docIndex: string, docId: string, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts index 6e284908e3358..90484a46dc6d3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts @@ -28,11 +28,11 @@ import { Version, MetaOrUndefined, RuleId, - ListAndOrUndefined, } from '../../../common/detection_engine/schemas/common/schemas'; import { CallAPIOptions } from '../../../../../../src/core/server'; import { Filter } from '../../../../../../src/plugins/data/server'; import { RuleType } from '../../../common/detection_engine/types'; +import { ListArrayOrUndefined } from '../../../common/detection_engine/schemas/types'; export type PartialFilter = Partial; @@ -62,7 +62,7 @@ export interface RuleTypeParams { type: RuleType; references: References; version: Version; - exceptionsList: ListAndOrUndefined; + exceptionsList: ListArrayOrUndefined; } // eslint-disable-next-line @typescript-eslint/no-explicit-any From d80c2babd39189476ff0885d719fd61130b39747 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Thu, 25 Jun 2020 12:14:10 -0400 Subject: [PATCH 40/82] [Maps] Remove broken button (#69853) (#69938) --- .../layer_panel/__snapshots__/view.test.js.snap | 14 +++----------- .../connected_components/layer_panel/index.js | 5 +---- .../connected_components/layer_panel/view.js | 16 ++-------------- .../plugins/translations/translations/ja-JP.json | 2 -- .../plugins/translations/translations/zh-CN.json | 2 -- 5 files changed, 6 insertions(+), 33 deletions(-) diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap index a9216e4817762..1620e3058be67 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap @@ -32,17 +32,9 @@ exports[`LayerPanel is rendered 1`] = ` - - - + { - dispatch(fitToLayerExtent(layerId)); - }, updateSourceProp: (id, propName, value, newLayerType) => dispatch(updateSourceProp(id, propName, value, newLayerType)), }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js index f34c402a4d417..14252dcfc067d 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js @@ -13,7 +13,7 @@ import { LayerErrors } from './layer_errors'; import { LayerSettings } from './layer_settings'; import { StyleSettings } from './style_settings'; import { - EuiButtonIcon, + EuiIcon, EuiFlexItem, EuiTitle, EuiPanel, @@ -27,7 +27,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; @@ -175,18 +174,7 @@ export class LayerPanel extends React.Component { - - - + diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b266a985e88ff..96e43819275c6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9009,8 +9009,6 @@ "xpack.maps.layerPanel.filterEditor.emptyState.description": "フィルターを追加してレイヤーデータを絞ります。", "xpack.maps.layerPanel.filterEditor.queryBarSubmitButtonLabel": "フィルターを設定", "xpack.maps.layerPanel.filterEditor.title": "フィルタリング", - "xpack.maps.layerPanel.fitToBoundsAriaLabel": "境界に合わせる", - "xpack.maps.layerPanel.fitToBoundsButtonLabel": "合わせる", "xpack.maps.layerPanel.footer.cancelButtonLabel": "キャンセル", "xpack.maps.layerPanel.footer.closeButtonLabel": "閉じる", "xpack.maps.layerPanel.footer.removeLayerButtonLabel": "レイヤーを削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ddd6114d8be26..9e78753c3655f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9013,8 +9013,6 @@ "xpack.maps.layerPanel.filterEditor.emptyState.description": "添加筛选以缩小图层数据范围。", "xpack.maps.layerPanel.filterEditor.queryBarSubmitButtonLabel": "设置筛选", "xpack.maps.layerPanel.filterEditor.title": "筛选", - "xpack.maps.layerPanel.fitToBoundsAriaLabel": "适应边界", - "xpack.maps.layerPanel.fitToBoundsButtonLabel": "适应", "xpack.maps.layerPanel.footer.cancelButtonLabel": "取消", "xpack.maps.layerPanel.footer.closeButtonLabel": "关闭", "xpack.maps.layerPanel.footer.removeLayerButtonLabel": "移除图层", From 096b4018206e198cb2551360440899aa10578239 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Thu, 25 Jun 2020 12:49:47 -0400 Subject: [PATCH 41/82] [7.x] [Monitoring] Love for APM (#69052) (#69741) * [Monitoring] Love for APM (#69052) * Fix broken colors for APM * Use default derivative * Fix UI issues with APM * Add new charts * Fix tests * Use EUI color palette * Remove old translations * PR feedback * Fix tests * Fix up overview page Co-authored-by: Elastic Machine * Fix eslint Co-authored-by: Elastic Machine --- .../components/apm/instance/instance.js | 16 +- .../components/apm/instances/instances.js | 6 +- .../public/components/apm/overview/index.js | 4 +- .../public/components/chart/get_color.js | 4 + .../monitoring/public/services/breadcrumbs.js | 2 + .../__snapshots__/metrics.test.js.snap | 506 ++++- .../server/lib/metrics/apm/classes.js | 7 +- .../server/lib/metrics/apm/metrics.js | 139 ++ .../server/routes/api/v1/apm/instance.js | 2 +- .../routes/api/v1/apm/metric_set_instance.js | 23 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../apis/monitoring/apm/fixtures/cluster.json | 1719 ++++++++++------- .../monitoring/apm/fixtures/instance.json | 1607 ++++++++------- 14 files changed, 2553 insertions(+), 1484 deletions(-) diff --git a/x-pack/plugins/monitoring/public/components/apm/instance/instance.js b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js index 47e8bc3b46474..40f6da476b468 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instance/instance.js +++ b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js @@ -19,18 +19,22 @@ import { Status } from './status'; export function ApmServerInstance({ summary, metrics, ...props }) { const seriesToShow = [ + metrics.apm_requests, metrics.apm_responses_valid, + metrics.apm_responses_errors, + metrics.apm_acm_request_count, + + metrics.apm_acm_response, + metrics.apm_acm_response_errors, metrics.apm_output_events_rate_success, metrics.apm_output_events_rate_failure, - metrics.apm_requests, metrics.apm_transformations, - metrics.apm_cpu, - metrics.apm_memory, + metrics.apm_memory, metrics.apm_os_load, ]; @@ -46,8 +50,10 @@ export function ApmServerInstance({ summary, metrics, ...props }) { - - + + + + {charts} diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js index bf5c4661b5a54..efca02f0041a3 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import moment from 'moment'; import { uniq, get } from 'lodash'; import { EuiMonitoringTable } from '../../table'; -import { EuiLink, EuiPage, EuiPageBody, EuiPageContent, EuiSpacer } from '@elastic/eui'; +import { EuiLink, EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiPanel } from '@elastic/eui'; import { Status } from './status'; import { formatMetric } from '../../../lib/format_number'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; @@ -138,7 +138,9 @@ export function ApmServerInstances({ apms, setupMode }) { - + + + {setupModeCallout} - + + + {charts} diff --git a/x-pack/plugins/monitoring/public/components/chart/get_color.js b/x-pack/plugins/monitoring/public/components/chart/get_color.js index e4a5777bb6efe..868b914a16c8a 100644 --- a/x-pack/plugins/monitoring/public/components/chart/get_color.js +++ b/x-pack/plugins/monitoring/public/components/chart/get_color.js @@ -13,10 +13,14 @@ * @param {Integer} index: index of the chart series, 0-3 * @returns {String} Hex color to use for chart series at the given index */ +import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; + export function getColor(app, index) { let seriesColors; if (app === 'elasticsearch') { seriesColors = ['#3ebeb0', '#3b73ac', '#f08656', '#6c478f']; + } else if (app === 'apm') { + seriesColors = euiPaletteColorBlind(); } else { // for kibana, and fallback (e.g., Logstash and Beats) seriesColors = ['#e8488b', '#3b73ac', '#3cab63', '#6c478f']; diff --git a/x-pack/plugins/monitoring/public/services/breadcrumbs.js b/x-pack/plugins/monitoring/public/services/breadcrumbs.js index 44422a42f5f0a..63bac2975dc20 100644 --- a/x-pack/plugins/monitoring/public/services/breadcrumbs.js +++ b/x-pack/plugins/monitoring/public/services/breadcrumbs.js @@ -76,6 +76,7 @@ function getKibanaBreadcrumbs(mainInstance) { }) ) ); + breadcrumbs.push(createCrumb(null, mainInstance.instance)); } else { // don't link to Overview when we're possibly on Overview or its sibling tabs breadcrumbs.push(createCrumb(null, 'Kibana')); @@ -160,6 +161,7 @@ function getApmBreadcrumbs(mainInstance) { }) ) ); + breadcrumbs.push(createCrumb(null, mainInstance.instance)); } else { // don't link to Overview when we're possibly on Overview or its sibling tabs breadcrumbs.push(createCrumb(null, apmLabel)); diff --git a/x-pack/plugins/monitoring/server/lib/metrics/__test__/__snapshots__/metrics.test.js.snap b/x-pack/plugins/monitoring/server/lib/metrics/__test__/__snapshots__/metrics.test.js.snap index 595ff3ccfced4..e2e49ce58cd12 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/__test__/__snapshots__/metrics.test.js.snap +++ b/x-pack/plugins/monitoring/server/lib/metrics/__test__/__snapshots__/metrics.test.js.snap @@ -2,6 +2,416 @@ exports[`Metrics should export metric objects that match a snapshot 1`] = ` Object { + "apm_acm_request_count": ApmEventsRateClusterMetric { + "aggs": Object { + "beats_uuids": Object { + "aggs": Object { + "event_rate_per_beat": Object { + "max": Object { + "field": "beats_stats.metrics.apm-server.acm.request.count", + }, + }, + }, + "terms": Object { + "field": "beats_stats.beat.uuid", + "size": 10000, + }, + }, + "event_rate": Object { + "sum_bucket": Object { + "buckets_path": "beats_uuids>event_rate_per_beat", + "gap_policy": "skip", + }, + }, + "metric_deriv": Object { + "derivative": Object { + "buckets_path": "event_rate", + "gap_policy": "skip", + "unit": "1s", + }, + }, + }, + "app": "apm", + "derivative": true, + "description": "HTTP Requests received by agent configuration managemen", + "field": "beats_stats.metrics.apm-server.acm.request.count", + "format": "0,0.[00]", + "label": "Count", + "metricAgg": "max", + "timestampField": "beats_stats.timestamp", + "title": "Requests Agent Configuration Management", + "units": "/s", + "uuidField": "cluster_uuid", + }, + "apm_acm_response_count": ApmEventsRateClusterMetric { + "aggs": Object { + "beats_uuids": Object { + "aggs": Object { + "event_rate_per_beat": Object { + "max": Object { + "field": "beats_stats.metrics.apm-server.acm.response.count", + }, + }, + }, + "terms": Object { + "field": "beats_stats.beat.uuid", + "size": 10000, + }, + }, + "event_rate": Object { + "sum_bucket": Object { + "buckets_path": "beats_uuids>event_rate_per_beat", + "gap_policy": "skip", + }, + }, + "metric_deriv": Object { + "derivative": Object { + "buckets_path": "event_rate", + "gap_policy": "skip", + "unit": "1s", + }, + }, + }, + "app": "apm", + "derivative": true, + "description": "HTTP requests responded to by APM Server", + "field": "beats_stats.metrics.apm-server.acm.response.count", + "format": "0,0.[00]", + "label": "Count", + "metricAgg": "max", + "timestampField": "beats_stats.timestamp", + "title": "Response Count Agent Configuration Management", + "units": "/s", + "uuidField": "cluster_uuid", + }, + "apm_acm_response_errors_count": ApmEventsRateClusterMetric { + "aggs": Object { + "beats_uuids": Object { + "aggs": Object { + "event_rate_per_beat": Object { + "max": Object { + "field": "beats_stats.metrics.apm-server.acm.response.errors.count", + }, + }, + }, + "terms": Object { + "field": "beats_stats.beat.uuid", + "size": 10000, + }, + }, + "event_rate": Object { + "sum_bucket": Object { + "buckets_path": "beats_uuids>event_rate_per_beat", + "gap_policy": "skip", + }, + }, + "metric_deriv": Object { + "derivative": Object { + "buckets_path": "event_rate", + "gap_policy": "skip", + "unit": "1s", + }, + }, + }, + "app": "apm", + "derivative": true, + "description": "HTTP errors count", + "field": "beats_stats.metrics.apm-server.acm.response.errors.count", + "format": "0,0.[00]", + "label": "Error Count", + "metricAgg": "max", + "timestampField": "beats_stats.timestamp", + "title": "Response Error Count Agent Configuration Management", + "units": "/s", + "uuidField": "cluster_uuid", + }, + "apm_acm_response_errors_forbidden": ApmEventsRateClusterMetric { + "aggs": Object { + "beats_uuids": Object { + "aggs": Object { + "event_rate_per_beat": Object { + "max": Object { + "field": "beats_stats.metrics.apm-server.acm.response.errors.forbidden", + }, + }, + }, + "terms": Object { + "field": "beats_stats.beat.uuid", + "size": 10000, + }, + }, + "event_rate": Object { + "sum_bucket": Object { + "buckets_path": "beats_uuids>event_rate_per_beat", + "gap_policy": "skip", + }, + }, + "metric_deriv": Object { + "derivative": Object { + "buckets_path": "event_rate", + "gap_policy": "skip", + "unit": "1s", + }, + }, + }, + "app": "apm", + "derivative": true, + "description": "Forbidden HTTP requests rejected count", + "field": "beats_stats.metrics.apm-server.acm.response.errors.forbidden", + "format": "0,0.[00]", + "label": "Count", + "metricAgg": "max", + "timestampField": "beats_stats.timestamp", + "title": "Response Errors Agent Configuration Management", + "units": "/s", + "uuidField": "cluster_uuid", + }, + "apm_acm_response_errors_invalidquery": ApmEventsRateClusterMetric { + "aggs": Object { + "beats_uuids": Object { + "aggs": Object { + "event_rate_per_beat": Object { + "max": Object { + "field": "beats_stats.metrics.apm-server.acm.response.errors.invalidquery", + }, + }, + }, + "terms": Object { + "field": "beats_stats.beat.uuid", + "size": 10000, + }, + }, + "event_rate": Object { + "sum_bucket": Object { + "buckets_path": "beats_uuids>event_rate_per_beat", + "gap_policy": "skip", + }, + }, + "metric_deriv": Object { + "derivative": Object { + "buckets_path": "event_rate", + "gap_policy": "skip", + "unit": "1s", + }, + }, + }, + "app": "apm", + "derivative": true, + "description": "Invalid HTTP query", + "field": "beats_stats.metrics.apm-server.acm.response.errors.invalidquery", + "format": "0,0.[00]", + "label": "Invalid Query", + "metricAgg": "max", + "timestampField": "beats_stats.timestamp", + "title": "Response Invalid Query Errors Agent Configuration Management", + "units": "/s", + "uuidField": "cluster_uuid", + }, + "apm_acm_response_errors_method": ApmEventsRateClusterMetric { + "aggs": Object { + "beats_uuids": Object { + "aggs": Object { + "event_rate_per_beat": Object { + "max": Object { + "field": "beats_stats.metrics.apm-server.acm.response.errors.method", + }, + }, + }, + "terms": Object { + "field": "beats_stats.beat.uuid", + "size": 10000, + }, + }, + "event_rate": Object { + "sum_bucket": Object { + "buckets_path": "beats_uuids>event_rate_per_beat", + "gap_policy": "skip", + }, + }, + "metric_deriv": Object { + "derivative": Object { + "buckets_path": "event_rate", + "gap_policy": "skip", + "unit": "1s", + }, + }, + }, + "app": "apm", + "derivative": true, + "description": "HTTP requests rejected due to incorrect HTTP method", + "field": "beats_stats.metrics.apm-server.acm.response.errors.method", + "format": "0,0.[00]", + "label": "Method", + "metricAgg": "max", + "timestampField": "beats_stats.timestamp", + "title": "Response Method Errors Agent Configuration Management", + "units": "/s", + "uuidField": "cluster_uuid", + }, + "apm_acm_response_errors_unauthorized": ApmEventsRateClusterMetric { + "aggs": Object { + "beats_uuids": Object { + "aggs": Object { + "event_rate_per_beat": Object { + "max": Object { + "field": "beats_stats.metrics.apm-server.acm.response.errors.unauthorized", + }, + }, + }, + "terms": Object { + "field": "beats_stats.beat.uuid", + "size": 10000, + }, + }, + "event_rate": Object { + "sum_bucket": Object { + "buckets_path": "beats_uuids>event_rate_per_beat", + "gap_policy": "skip", + }, + }, + "metric_deriv": Object { + "derivative": Object { + "buckets_path": "event_rate", + "gap_policy": "skip", + "unit": "1s", + }, + }, + }, + "app": "apm", + "derivative": true, + "description": "Unauthorized HTTP requests rejected count", + "field": "beats_stats.metrics.apm-server.acm.response.errors.unauthorized", + "format": "0,0.[00]", + "label": "Unauthorized", + "metricAgg": "max", + "timestampField": "beats_stats.timestamp", + "title": "Response Unauthorized Errors Agent Configuration Management", + "units": "/s", + "uuidField": "cluster_uuid", + }, + "apm_acm_response_errors_unavailable": ApmEventsRateClusterMetric { + "aggs": Object { + "beats_uuids": Object { + "aggs": Object { + "event_rate_per_beat": Object { + "max": Object { + "field": "beats_stats.metrics.apm-server.acm.response.errors.unavailable", + }, + }, + }, + "terms": Object { + "field": "beats_stats.beat.uuid", + "size": 10000, + }, + }, + "event_rate": Object { + "sum_bucket": Object { + "buckets_path": "beats_uuids>event_rate_per_beat", + "gap_policy": "skip", + }, + }, + "metric_deriv": Object { + "derivative": Object { + "buckets_path": "event_rate", + "gap_policy": "skip", + "unit": "1s", + }, + }, + }, + "app": "apm", + "derivative": true, + "description": "Unavailable HTTP response count. Possible misconfiguration or unsupported version of Kibana", + "field": "beats_stats.metrics.apm-server.acm.response.errors.unavailable", + "format": "0,0.[00]", + "label": "Unavailable", + "metricAgg": "max", + "timestampField": "beats_stats.timestamp", + "title": "Response Unavailable Errors Agent Configuration Management", + "units": "/s", + "uuidField": "cluster_uuid", + }, + "apm_acm_response_valid_notmodified": ApmEventsRateClusterMetric { + "aggs": Object { + "beats_uuids": Object { + "aggs": Object { + "event_rate_per_beat": Object { + "max": Object { + "field": "beats_stats.metrics.apm-server.acm.response.valid.notmodified", + }, + }, + }, + "terms": Object { + "field": "beats_stats.beat.uuid", + "size": 10000, + }, + }, + "event_rate": Object { + "sum_bucket": Object { + "buckets_path": "beats_uuids>event_rate_per_beat", + "gap_policy": "skip", + }, + }, + "metric_deriv": Object { + "derivative": Object { + "buckets_path": "event_rate", + "gap_policy": "skip", + "unit": "1s", + }, + }, + }, + "app": "apm", + "derivative": true, + "description": "304 Not modified response count", + "field": "beats_stats.metrics.apm-server.acm.response.valid.notmodified", + "format": "0,0.[00]", + "label": "Not Modified", + "metricAgg": "max", + "timestampField": "beats_stats.timestamp", + "title": "Response Not Modified Agent Configuration Management", + "units": "/s", + "uuidField": "cluster_uuid", + }, + "apm_acm_response_valid_ok": ApmEventsRateClusterMetric { + "aggs": Object { + "beats_uuids": Object { + "aggs": Object { + "event_rate_per_beat": Object { + "max": Object { + "field": "beats_stats.metrics.apm-server.acm.response.valid.ok", + }, + }, + }, + "terms": Object { + "field": "beats_stats.beat.uuid", + "size": 10000, + }, + }, + "event_rate": Object { + "sum_bucket": Object { + "buckets_path": "beats_uuids>event_rate_per_beat", + "gap_policy": "skip", + }, + }, + "metric_deriv": Object { + "derivative": Object { + "buckets_path": "event_rate", + "gap_policy": "skip", + "unit": "1s", + }, + }, + }, + "app": "apm", + "derivative": true, + "description": "200 OK response count", + "field": "beats_stats.metrics.apm-server.acm.response.valid.ok", + "format": "0,0.[00]", + "label": "OK", + "metricAgg": "max", + "timestampField": "beats_stats.timestamp", + "title": "Response OK Count Agent Configuration Management", + "units": "/s", + "uuidField": "cluster_uuid", + }, "apm_cpu_total": ApmCpuUtilizationMetric { "app": "apm", "calculation": [Function], @@ -80,7 +490,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -93,7 +503,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Output Acked Events Rate", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_output_events_active": ApmEventsRateClusterMetric { @@ -121,7 +531,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -134,7 +544,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Output Active Events Rate", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_output_events_dropped": ApmEventsRateClusterMetric { @@ -162,7 +572,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -175,7 +585,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Output Dropped Events Rate", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_output_events_failed": ApmEventsRateClusterMetric { @@ -203,7 +613,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -216,7 +626,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Output Failed Events Rate", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_output_events_total": ApmEventsRateClusterMetric { @@ -244,7 +654,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -257,7 +667,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Output Events Rate", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_processor_error_transformations": ApmEventsRateClusterMetric { @@ -285,7 +695,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -298,7 +708,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Transformations", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_processor_metric_transformations": ApmEventsRateClusterMetric { @@ -326,7 +736,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -339,7 +749,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Transformations", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_processor_span_transformations": ApmEventsRateClusterMetric { @@ -367,7 +777,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -380,7 +790,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Transformations", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_processor_transaction_transformations": ApmEventsRateClusterMetric { @@ -408,7 +818,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -421,7 +831,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Processed Events", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_requests": ApmEventsRateClusterMetric { @@ -449,7 +859,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -462,7 +872,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Request Count Intake API", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_count": ApmEventsRateClusterMetric { @@ -490,7 +900,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -503,7 +913,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Response Count Intake API", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_errors_closed": ApmEventsRateClusterMetric { @@ -531,7 +941,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -544,7 +954,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Closed", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_errors_concurrency": ApmEventsRateClusterMetric { @@ -572,7 +982,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -585,7 +995,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Concurrency", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_errors_decode": ApmEventsRateClusterMetric { @@ -613,7 +1023,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -626,7 +1036,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Decode", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_errors_forbidden": ApmEventsRateClusterMetric { @@ -654,7 +1064,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -667,7 +1077,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Forbidden", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_errors_internal": ApmEventsRateClusterMetric { @@ -695,7 +1105,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -708,7 +1118,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Internal", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_errors_method": ApmEventsRateClusterMetric { @@ -736,7 +1146,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -749,7 +1159,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Method", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_errors_queue": ApmEventsRateClusterMetric { @@ -777,7 +1187,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -790,7 +1200,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Queue", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_errors_ratelimit": ApmEventsRateClusterMetric { @@ -818,7 +1228,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -831,7 +1241,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Rate limit", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_errors_toolarge": ApmEventsRateClusterMetric { @@ -859,7 +1269,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -872,7 +1282,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Response Errors Intake API", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_errors_unauthorized": ApmEventsRateClusterMetric { @@ -900,7 +1310,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -913,7 +1323,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Unauthorized", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_errors_validate": ApmEventsRateClusterMetric { @@ -941,7 +1351,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -954,7 +1364,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Validate", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_valid_accepted": ApmEventsRateClusterMetric { @@ -982,7 +1392,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -995,7 +1405,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Accepted", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_responses_valid_ok": ApmEventsRateClusterMetric { @@ -1023,7 +1433,7 @@ Object { "derivative": Object { "buckets_path": "event_rate", "gap_policy": "skip", - "unit": "1m", + "unit": "1s", }, }, }, @@ -1036,7 +1446,7 @@ Object { "metricAgg": "max", "timestampField": "beats_stats.timestamp", "title": "Ok", - "units": "/m", + "units": "/s", "uuidField": "cluster_uuid", }, "apm_system_os_load_1": ApmMetric { diff --git a/x-pack/plugins/monitoring/server/lib/metrics/apm/classes.js b/x-pack/plugins/monitoring/server/lib/metrics/apm/classes.js index fc8a45669bd94..840f5d5cb239e 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/apm/classes.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/apm/classes.js @@ -7,6 +7,7 @@ import { ClusterMetric, Metric } from '../classes'; import { SMALL_FLOAT, LARGE_FLOAT } from '../../../../common/formatting'; import { i18n } from '@kbn/i18n'; +import { NORMALIZED_DERIVATIVE_UNIT } from '../../../../common/constants'; export class ApmClusterMetric extends ClusterMetric { constructor(opts) { @@ -76,8 +77,8 @@ export class ApmEventsRateClusterMetric extends ApmClusterMetric { derivative: true, format: LARGE_FLOAT, metricAgg: 'max', - units: i18n.translate('xpack.monitoring.metrics.apm.perMinuteUnitLabel', { - defaultMessage: '/m', + units: i18n.translate('xpack.monitoring.metrics.apm.perSecondUnitLabel', { + defaultMessage: '/s', }), }); @@ -105,7 +106,7 @@ export class ApmEventsRateClusterMetric extends ApmClusterMetric { derivative: { buckets_path: 'event_rate', gap_policy: 'skip', - unit: '1m', + unit: NORMALIZED_DERIVATIVE_UNIT, }, }, }; diff --git a/x-pack/plugins/monitoring/server/lib/metrics/apm/metrics.js b/x-pack/plugins/monitoring/server/lib/metrics/apm/metrics.js index db2eae591f359..1d063a57bbb5b 100644 --- a/x-pack/plugins/monitoring/server/lib/metrics/apm/metrics.js +++ b/x-pack/plugins/monitoring/server/lib/metrics/apm/metrics.js @@ -449,4 +449,143 @@ export const metrics = { } ), }), + apm_acm_response_count: new ApmEventsRateClusterMetric({ + field: 'beats_stats.metrics.apm-server.acm.response.count', + title: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.countTitle', { + defaultMessage: 'Response Count Agent Configuration Management', + }), + label: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.countLabel', { + defaultMessage: 'Count', + }), + description: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.countDescription', { + defaultMessage: 'HTTP requests responded to by APM Server', + }), + }), + apm_acm_response_errors_count: new ApmEventsRateClusterMetric({ + field: 'beats_stats.metrics.apm-server.acm.response.errors.count', + title: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errorCountTitle', { + defaultMessage: 'Response Error Count Agent Configuration Management', + }), + label: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errorCountLabel', { + defaultMessage: 'Error Count', + }), + description: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errorCountDescription', { + defaultMessage: 'HTTP errors count', + }), + }), + apm_acm_response_valid_ok: new ApmEventsRateClusterMetric({ + field: 'beats_stats.metrics.apm-server.acm.response.valid.ok', + title: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.validOkTitle', { + defaultMessage: 'Response OK Count Agent Configuration Management', + }), + label: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.validOkLabel', { + defaultMessage: 'OK', + }), + description: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.validOkDescription', { + defaultMessage: '200 OK response count', + }), + }), + apm_acm_response_valid_notmodified: new ApmEventsRateClusterMetric({ + field: 'beats_stats.metrics.apm-server.acm.response.valid.notmodified', + title: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.validNotModifiedTitle', { + defaultMessage: 'Response Not Modified Agent Configuration Management', + }), + label: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.validNotModifiedLabel', { + defaultMessage: 'Not Modified', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.apm.acmResponse.validNotModifiedDescription', + { + defaultMessage: '304 Not modified response count', + } + ), + }), + apm_acm_response_errors_forbidden: new ApmEventsRateClusterMetric({ + field: 'beats_stats.metrics.apm-server.acm.response.errors.forbidden', + title: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errors.forbiddenTitle', { + defaultMessage: 'Response Errors Agent Configuration Management', + }), + label: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errors.forbiddenLabel', { + defaultMessage: 'Count', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.apm.acmResponse.errors.forbiddenDescription', + { + defaultMessage: 'Forbidden HTTP requests rejected count', + } + ), + }), + apm_acm_response_errors_unauthorized: new ApmEventsRateClusterMetric({ + field: 'beats_stats.metrics.apm-server.acm.response.errors.unauthorized', + title: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errors.unauthorizedTitle', { + defaultMessage: 'Response Unauthorized Errors Agent Configuration Management', + }), + label: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errors.unauthorizedLabel', { + defaultMessage: 'Unauthorized', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.apm.acmResponse.errors.unauthorizedDescription', + { + defaultMessage: 'Unauthorized HTTP requests rejected count', + } + ), + }), + apm_acm_response_errors_unavailable: new ApmEventsRateClusterMetric({ + field: 'beats_stats.metrics.apm-server.acm.response.errors.unavailable', + title: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errors.unavailableTitle', { + defaultMessage: 'Response Unavailable Errors Agent Configuration Management', + }), + label: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errors.unavailableLabel', { + defaultMessage: 'Unavailable', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.apm.acmResponse.errors.unavailableDescription', + { + defaultMessage: + 'Unavailable HTTP response count. Possible misconfiguration or unsupported version of Kibana', + } + ), + }), + apm_acm_response_errors_method: new ApmEventsRateClusterMetric({ + field: 'beats_stats.metrics.apm-server.acm.response.errors.method', + title: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errors.methodTitle', { + defaultMessage: 'Response Method Errors Agent Configuration Management', + }), + label: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errors.methodLabel', { + defaultMessage: 'Method', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.apm.acmResponse.errors.methodDescription', + { + defaultMessage: 'HTTP requests rejected due to incorrect HTTP method', + } + ), + }), + apm_acm_response_errors_invalidquery: new ApmEventsRateClusterMetric({ + field: 'beats_stats.metrics.apm-server.acm.response.errors.invalidquery', + title: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errors.invalidqueryTitle', { + defaultMessage: 'Response Invalid Query Errors Agent Configuration Management', + }), + label: i18n.translate('xpack.monitoring.metrics.apm.acmResponse.errors.invalidqueryLabel', { + defaultMessage: 'Invalid Query', + }), + description: i18n.translate( + 'xpack.monitoring.metrics.apm.acmResponse.errors.invalidqueryDescription', + { + defaultMessage: 'Invalid HTTP query', + } + ), + }), + apm_acm_request_count: new ApmEventsRateClusterMetric({ + field: 'beats_stats.metrics.apm-server.acm.request.count', + title: i18n.translate('xpack.monitoring.metrics.apm.acmRequest.countTitle', { + defaultMessage: 'Requests Agent Configuration Management', + }), + label: i18n.translate('xpack.monitoring.metrics.apm.acmRequest.countTitleLabel', { + defaultMessage: 'Count', + }), + description: i18n.translate('xpack.monitoring.metrics.apm.acmRequest.countTitleDescription', { + defaultMessage: 'HTTP Requests received by agent configuration managemen', + }), + }), }; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js index 0ff9e834b924c..16921e998f296 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/instance.js @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { prefixIndexPattern } from '../../../../lib/ccs_utils'; import { getMetrics } from '../../../../lib/details/get_metrics'; -import { metricSet } from './metric_set_overview'; +import { metricSet } from './metric_set_instance'; import { handleError } from '../../../../lib/errors'; import { getApmInfo } from '../../../../lib/apm'; import { INDEX_PATTERN_BEATS } from '../../../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/apm/metric_set_instance.js b/x-pack/plugins/monitoring/server/routes/api/v1/apm/metric_set_instance.js index 9ce96d386ae2f..5ace433c295da 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/apm/metric_set_instance.js +++ b/x-pack/plugins/monitoring/server/routes/api/v1/apm/metric_set_instance.js @@ -59,4 +59,27 @@ export const metricSet = [ ], name: 'apm_transformations', }, + { + keys: [ + 'apm_acm_response_count', + 'apm_acm_response_errors_count', + 'apm_acm_response_valid_ok', + 'apm_acm_response_valid_notmodified', + ], + name: 'apm_acm_response', + }, + { + keys: [ + 'apm_acm_response_errors_forbidden', + 'apm_acm_response_errors_unauthorized', + 'apm_acm_response_errors_unavailable', + 'apm_acm_response_errors_method', + 'apm_acm_response_errors_invalidquery', + ], + name: 'apm_acm_response_errors', + }, + { + keys: ['apm_acm_request_count'], + name: 'apm_acm_request_count', + }, ]; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 96e43819275c6..8c9110e4e3b48 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11701,7 +11701,6 @@ "xpack.monitoring.metrics.apm.outputFailedEventsRate.failedDescription": "アウトプットにより処理されたイベントです (再試行を含む)", "xpack.monitoring.metrics.apm.outputFailedEventsRate.failedLabel": "失敗", "xpack.monitoring.metrics.apm.outputFailedEventsRateTitle": "アウトプットイベント失敗率", - "xpack.monitoring.metrics.apm.perMinuteUnitLabel": "/m", "xpack.monitoring.metrics.apm.processedEvents.transactionDescription": "処理されたトランザクションイベントです", "xpack.monitoring.metrics.apm.processedEvents.transactionLabel": "トランザクション", "xpack.monitoring.metrics.apm.processedEventsTitle": "処理済みのイベント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9e78753c3655f..94be00dd2a6da 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -11706,7 +11706,6 @@ "xpack.monitoring.metrics.apm.outputFailedEventsRate.failedDescription": "输出处理的事件(包括重试)", "xpack.monitoring.metrics.apm.outputFailedEventsRate.failedLabel": "失败", "xpack.monitoring.metrics.apm.outputFailedEventsRateTitle": "输出失败事件速率", - "xpack.monitoring.metrics.apm.perMinuteUnitLabel": "/分钟", "xpack.monitoring.metrics.apm.processedEvents.transactionDescription": "已处理事务事件", "xpack.monitoring.metrics.apm.processedEvents.transactionLabel": "事务", "xpack.monitoring.metrics.apm.processedEventsTitle": "已处理事件", diff --git a/x-pack/test/api_integration/apis/monitoring/apm/fixtures/cluster.json b/x-pack/test/api_integration/apis/monitoring/apm/fixtures/cluster.json index aa9e4cc125a90..e54a1c2210d48 100644 --- a/x-pack/test/api_integration/apis/monitoring/apm/fixtures/cluster.json +++ b/x-pack/test/api_integration/apis/monitoring/apm/fixtures/cluster.json @@ -9,727 +9,1046 @@ "timeOfLastEvent": "2018-08-31T13:59:21.201Z" }, "metrics": { - "apm_cpu": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 + "apm_cpu": [ + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.beat.cpu.total.value", + "metricAgg": "max", + "label": "Total", + "title": "CPU Utilization", + "description": "Percentage of CPU time spent executing (user+kernel mode) for the APM process", + "units": "%", + "format": "0.[00]", + "hasCalculation": true, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0.1 + ], + [ + 1535723940000, + 0.26666666666666666 + ] + ] + } + ], + "apm_os_load": [ + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.system.load.1", + "metricAgg": "max", + "label": "1m", + "title": "System Load", + "description": "Load average over the last 1 minute", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1535723880000, + 1.37 + ], + [ + 1535723910000, + 1.01 + ], + [ + 1535723940000, + 0.61 + ] + ] }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.beat.cpu.total.value", - "metricAgg": "max", - "label": "Total", - "title": "CPU Utilization", - "description": "Percentage of CPU time spent executing (user+kernel mode) for the APM process", - "units": "%", - "format": "0.[00]", - "hasCalculation": true, - "isDerivative": true + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.system.load.5", + "metricAgg": "max", + "label": "5m", + "title": "System Load", + "description": "Load average over the last 5 minutes", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1535723880000, + 1.72 + ], + [ + 1535723910000, + 1.6 + ], + [ + 1535723940000, + 1.45 + ] + ] }, - "data": [ - [1535723880000, null], - [1535723910000, 0.1], - [1535723940000, 0.26666666666666666] - ] - }], - "apm_os_load": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.system.load.15", + "metricAgg": "max", + "label": "15m", + "title": "System Load", + "description": "Load average over the last 15 minutes", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1535723880000, + 2.5 + ], + [ + 1535723910000, + 2.43 + ], + [ + 1535723940000, + 2.35 + ] + ] + } + ], + "apm_memory": [ + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.beat.memstats.memory_alloc", + "metricAgg": "max", + "label": "Allocated Memory", + "title": "Memory", + "description": "Allocated memory", + "units": "B", + "format": "0,0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1535723880000, + 4660952 + ], + [ + 1535723910000, + 3888048 + ], + [ + 1535723940000, + 3445920 + ] + ] }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.system.load.1", - "metricAgg": "max", - "label": "1m", - "title": "System Load", - "description": "Load average over the last 1 minute", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": false + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.beat.memstats.rss", + "metricAgg": "max", + "label": "Process Total", + "title": "Memory", + "description": "Resident set size of memory reserved by the APM service from the OS", + "units": "B", + "format": "0,0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1535723880000, + 10866688 + ], + [ + 1535723910000, + 11456512 + ], + [ + 1535723940000, + 12095488 + ] + ] }, - "data": [ - [1535723880000, 1.37], - [1535723910000, 1.01], - [1535723940000, 0.61] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.beat.memstats.gc_next", + "metricAgg": "max", + "label": "GC Next", + "title": "Memory", + "description": "Limit of allocated memory at which garbage collection will occur", + "units": "B", + "format": "0,0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [ + 1535723880000, + 5212816 + ], + [ + 1535723910000, + 4996912 + ], + [ + 1535723940000, + 4886176 + ] + ] + } + ], + "apm_output_events_rate_success": [ + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.libbeat.output.events.total", + "metricAgg": "max", + "label": "Total", + "title": "Output Events Rate", + "description": "Events processed by the output (including retries)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0.3 + ], + [ + 1535723940000, + 0.2 + ] + ] }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.system.load.5", - "metricAgg": "max", - "label": "5m", - "title": "System Load", - "description": "Load average over the last 5 minutes", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": false + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.libbeat.output.events.active", + "metricAgg": "max", + "label": "Active", + "title": "Output Active Events Rate", + "description": "Events processed by the output (including retries)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + null + ], + [ + 1535723940000, + 0 + ] + ] }, - "data": [ - [1535723880000, 1.72], - [1535723910000, 1.6], - [1535723940000, 1.45] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.libbeat.output.events.acked", + "metricAgg": "max", + "label": "Acked", + "title": "Output Acked Events Rate", + "description": "Events processed by the output (including retries)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0.3 + ], + [ + 1535723940000, + 0.2 + ] + ] + } + ], + "apm_output_events_rate_failure": [ + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.libbeat.output.events.failed", + "metricAgg": "max", + "label": "Failed", + "title": "Output Failed Events Rate", + "description": "Events processed by the output (including retries)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.system.load.15", - "metricAgg": "max", - "label": "15m", - "title": "System Load", - "description": "Load average over the last 15 minutes", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": false + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.libbeat.output.events.dropped", + "metricAgg": "max", + "label": "Dropped", + "title": "Output Dropped Events Rate", + "description": "Events processed by the output (including retries)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] + } + ], + "apm_responses_valid": [ + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.count", + "metricAgg": "max", + "label": "Total", + "title": "Response Count Intake API", + "description": "HTTP Requests responded to by server", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 1.4 + ], + [ + 1535723940000, + 1.4 + ] + ] }, - "data": [ - [1535723880000, 2.5], - [1535723910000, 2.43], - [1535723940000, 2.35] - ] - }], - "apm_memory": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.valid.ok", + "metricAgg": "max", + "label": "Ok", + "title": "Ok", + "description": "200 OK response count", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 1.3333333333333333 + ], + [ + 1535723940000, + 1.3666666666666667 + ] + ] }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.beat.memstats.memory_alloc", - "metricAgg": "max", - "label": "Allocated Memory", - "title": "Memory", - "description": "Allocated memory", - "units": "B", - "format": "0,0.0 b", - "hasCalculation": false, - "isDerivative": false + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.valid.accepted", + "metricAgg": "max", + "label": "Accepted", + "title": "Accepted", + "description": "HTTP Requests successfully reporting new events", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0.06666666666666667 + ], + [ + 1535723940000, + 0.03333333333333333 + ] + ] + } + ], + "apm_responses_errors": [ + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.toolarge", + "metricAgg": "max", + "label": "Too large", + "title": "Response Errors Intake API", + "description": "HTTP Requests rejected due to excessive payload size", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] }, - "data": [ - [1535723880000, 4660952], - [1535723910000, 3888048], - [1535723940000, 3445920] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.validate", + "metricAgg": "max", + "label": "Validate", + "title": "Validate", + "description": "HTTP Requests rejected due to payload validation error", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.beat.memstats.rss", - "metricAgg": "max", - "label": "Process Total", - "title": "Memory", - "description": "Resident set size of memory reserved by the APM service from the OS", - "units": "B", - "format": "0,0.0 b", - "hasCalculation": false, - "isDerivative": false + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.method", + "metricAgg": "max", + "label": "Method", + "title": "Method", + "description": "HTTP Requests rejected due to incorrect HTTP method", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] }, - "data": [ - [1535723880000, 10866688], - [1535723910000, 11456512], - [1535723940000, 12095488] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.unauthorized", + "metricAgg": "max", + "label": "Unauthorized", + "title": "Unauthorized", + "description": "HTTP Requests rejected due to invalid secret token", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.beat.memstats.gc_next", - "metricAgg": "max", - "label": "GC Next", - "title": "Memory", - "description": "Limit of allocated memory at which garbage collection will occur", - "units": "B", - "format": "0,0.0 b", - "hasCalculation": false, - "isDerivative": false + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.ratelimit", + "metricAgg": "max", + "label": "Rate limit", + "title": "Rate limit", + "description": "HTTP Requests rejected to due excessive rate limit", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] }, - "data": [ - [1535723880000, 5212816], - [1535723910000, 4996912], - [1535723940000, 4886176] - ] - }], - "apm_output_events_rate_success": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.queue", + "metricAgg": "max", + "label": "Queue", + "title": "Queue", + "description": "HTTP Requests rejected to due internal queue filling up", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.libbeat.output.events.total", - "metricAgg": "max", - "label": "Total", - "title": "Output Events Rate", - "description": "Events processed by the output (including retries)", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.decode", + "metricAgg": "max", + "label": "Decode", + "title": "Decode", + "description": "HTTP Requests rejected to due decoding errors - invalid json, incorrect data type for entity", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] }, - "data": [ - [1535723880000, null], - [1535723910000, 18], - [1535723940000, 12] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.forbidden", + "metricAgg": "max", + "label": "Forbidden", + "title": "Forbidden", + "description": "Forbidden HTTP Requests rejected - CORS violation, disabled enpoint", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.libbeat.output.events.active", - "metricAgg": "max", - "label": "Active", - "title": "Output Active Events Rate", - "description": "Events processed by the output (including retries)", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.concurrency", + "metricAgg": "max", + "label": "Concurrency", + "title": "Concurrency", + "description": "HTTP Requests rejected due to overall concurrency limit breach", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] }, - "data": [ - [1535723880000, null], - [1535723910000, null], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.closed", + "metricAgg": "max", + "label": "Closed", + "title": "Closed", + "description": "HTTP Requests rejected during server shutdown", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.libbeat.output.events.acked", - "metricAgg": "max", - "label": "Acked", - "title": "Output Acked Events Rate", - "description": "Events processed by the output (including retries)", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.internal", + "metricAgg": "max", + "label": "Internal", + "title": "Internal", + "description": "HTTP Requests rejected due to a miscellaneous internal error", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] + } + ], + "apm_requests": [ + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.request.count", + "metricAgg": "max", + "label": "Requested", + "title": "Request Count Intake API", + "description": "HTTP Requests received by server", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 1.4 + ], + [ + 1535723940000, + 1.4 + ] + ] + } + ], + "apm_transformations": [ + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.processor.transaction.transformations", + "metricAgg": "max", + "label": "Transaction", + "title": "Processed Events", + "description": "Transaction events processed", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0.1 + ], + [ + 1535723940000, + 0.06666666666666667 + ] + ] }, - "data": [ - [1535723880000, null], - [1535723910000, 18], - [1535723940000, 12] - ] - }], - "apm_output_events_rate_failure": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.processor.span.transformations", + "metricAgg": "max", + "label": "Span", + "title": "Transformations", + "description": "Span events processed", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0.2 + ], + [ + 1535723940000, + 0.13333333333333333 + ] + ] }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.libbeat.output.events.failed", - "metricAgg": "max", - "label": "Failed", - "title": "Output Failed Events Rate", - "description": "Events processed by the output (including retries)", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.processor.error.transformations", + "metricAgg": "max", + "label": "Error", + "title": "Transformations", + "description": "Error events processed", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.libbeat.output.events.dropped", - "metricAgg": "max", - "label": "Dropped", - "title": "Output Dropped Events Rate", - "description": "Events processed by the output (including retries)", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }], - "apm_responses_valid": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.count", - "metricAgg": "max", - "label": "Total", - "title": "Response Count Intake API", - "description": "HTTP Requests responded to by server", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 84], - [1535723940000, 84] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.valid.ok", - "metricAgg": "max", - "label": "Ok", - "title": "Ok", - "description": "200 OK response count", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 80], - [1535723940000, 82] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.valid.accepted", - "metricAgg": "max", - "label": "Accepted", - "title": "Accepted", - "description": "HTTP Requests successfully reporting new events", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 4], - [1535723940000, 2] - ] - }], - "apm_responses_errors": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.toolarge", - "metricAgg": "max", - "label": "Too large", - "title": "Response Errors Intake API", - "description": "HTTP Requests rejected due to excessive payload size", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.validate", - "metricAgg": "max", - "label": "Validate", - "title": "Validate", - "description": "HTTP Requests rejected due to payload validation error", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.method", - "metricAgg": "max", - "label": "Method", - "title": "Method", - "description": "HTTP Requests rejected due to incorrect HTTP method", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.unauthorized", - "metricAgg": "max", - "label": "Unauthorized", - "title": "Unauthorized", - "description": "HTTP Requests rejected due to invalid secret token", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.ratelimit", - "metricAgg": "max", - "label": "Rate limit", - "title": "Rate limit", - "description": "HTTP Requests rejected to due excessive rate limit", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.queue", - "metricAgg": "max", - "label": "Queue", - "title": "Queue", - "description": "HTTP Requests rejected to due internal queue filling up", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.decode", - "metricAgg": "max", - "label": "Decode", - "title": "Decode", - "description": "HTTP Requests rejected to due decoding errors - invalid json, incorrect data type for entity", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.forbidden", - "metricAgg": "max", - "label": "Forbidden", - "title": "Forbidden", - "description": "Forbidden HTTP Requests rejected - CORS violation, disabled enpoint", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.concurrency", - "metricAgg": "max", - "label": "Concurrency", - "title": "Concurrency", - "description": "HTTP Requests rejected due to overall concurrency limit breach", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.closed", - "metricAgg": "max", - "label": "Closed", - "title": "Closed", - "description": "HTTP Requests rejected during server shutdown", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.internal", - "metricAgg": "max", - "label": "Internal", - "title": "Internal", - "description": "HTTP Requests rejected due to a miscellaneous internal error", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }], - "apm_requests": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.request.count", - "metricAgg": "max", - "label": "Requested", - "title": "Request Count Intake API", - "description": "HTTP Requests received by server", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 84], - [1535723940000, 84] - ] - }], - "apm_transformations": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.processor.transaction.transformations", - "metricAgg": "max", - "label": "Transaction", - "title": "Processed Events", - "description": "Transaction events processed", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 6], - [1535723940000, 4] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.processor.span.transformations", - "metricAgg": "max", - "label": "Span", - "title": "Transformations", - "description": "Span events processed", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 12], - [1535723940000, 8] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.processor.error.transformations", - "metricAgg": "max", - "label": "Error", - "title": "Transformations", - "description": "Error events processed", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.processor.metric.transformations", - "metricAgg": "max", - "label": "Metric", - "title": "Transformations", - "description": "Metric events processed", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }] + { + "bucket_size": "30 seconds", + "timeRange": { + "min": 1535720389104, + "max": 1535723989104 + }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.processor.metric.transformations", + "metricAgg": "max", + "label": "Metric", + "title": "Transformations", + "description": "Metric events processed", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [ + 1535723880000, + null + ], + [ + 1535723910000, + 0 + ], + [ + 1535723940000, + 0 + ] + ] + } + ] } } diff --git a/x-pack/test/api_integration/apis/monitoring/apm/fixtures/instance.json b/x-pack/test/api_integration/apis/monitoring/apm/fixtures/instance.json index abcbf7557234d..f1747507b71d5 100644 --- a/x-pack/test/api_integration/apis/monitoring/apm/fixtures/instance.json +++ b/x-pack/test/api_integration/apis/monitoring/apm/fixtures/instance.json @@ -1,727 +1,890 @@ { "metrics": { - "apm_cpu": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.beat.cpu.total.value", - "metricAgg": "max", - "label": "Total", - "title": "CPU Utilization", - "description": "Percentage of CPU time spent executing (user+kernel mode) for the APM process", - "units": "%", - "format": "0.[00]", - "hasCalculation": true, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0.13333333333333333], - [1535723940000, 0.3] - ] - }], - "apm_os_load": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.system.load.1", - "metricAgg": "max", - "label": "1m", - "title": "System Load", - "description": "Load average over the last 1 minute", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1535723880000, 1.37], - [1535723910000, 1.01], - [1535723940000, 0.61] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.system.load.5", - "metricAgg": "max", - "label": "5m", - "title": "System Load", - "description": "Load average over the last 5 minutes", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1535723880000, 1.72], - [1535723910000, 1.6], - [1535723940000, 1.45] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.system.load.15", - "metricAgg": "max", - "label": "15m", - "title": "System Load", - "description": "Load average over the last 15 minutes", - "units": "", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1535723880000, 2.5], - [1535723910000, 2.43], - [1535723940000, 2.35] - ] - }], - "apm_memory": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.beat.memstats.memory_alloc", - "metricAgg": "max", - "label": "Allocated Memory", - "title": "Memory", - "description": "Allocated memory", - "units": "B", - "format": "0,0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1535723880000, 4421336], - [1535723910000, 3888048], - [1535723940000, 3087640] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.beat.memstats.rss", - "metricAgg": "max", - "label": "Process Total", - "title": "Memory", - "description": "Resident set size of memory reserved by the APM service from the OS", - "units": "B", - "format": "0,0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1535723880000, 10260480], - [1535723910000, 11456512], - [1535723940000, 12095488] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.beat.memstats.gc_next", - "metricAgg": "max", - "label": "GC Next", - "title": "Memory", - "description": "Limit of allocated memory at which garbage collection will occur", - "units": "B", - "format": "0,0.0 b", - "hasCalculation": false, - "isDerivative": false - }, - "data": [ - [1535723880000, 4996912], - [1535723910000, 4996912], - [1535723940000, 4886176] - ] - }], - "apm_output_events_rate_success": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.libbeat.output.events.total", - "metricAgg": "max", - "label": "Total", - "title": "Output Events Rate", - "description": "Events processed by the output (including retries)", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 6], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.libbeat.output.events.active", - "metricAgg": "max", - "label": "Active", - "title": "Output Active Events Rate", - "description": "Events processed by the output (including retries)", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.libbeat.output.events.acked", - "metricAgg": "max", - "label": "Acked", - "title": "Output Acked Events Rate", - "description": "Events processed by the output (including retries)", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 6], - [1535723940000, 0] - ] - }], - "apm_output_events_rate_failure": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.libbeat.output.events.failed", - "metricAgg": "max", - "label": "Failed", - "title": "Output Failed Events Rate", - "description": "Events processed by the output (including retries)", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.libbeat.output.events.dropped", - "metricAgg": "max", - "label": "Dropped", - "title": "Output Dropped Events Rate", - "description": "Events processed by the output (including retries)", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }], - "apm_responses_valid": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.count", - "metricAgg": "max", - "label": "Total", - "title": "Response Count Intake API", - "description": "HTTP Requests responded to by server", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 42], - [1535723940000, 42] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.valid.ok", - "metricAgg": "max", - "label": "Ok", - "title": "Ok", - "description": "200 OK response count", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 40], - [1535723940000, 42] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.valid.accepted", - "metricAgg": "max", - "label": "Accepted", - "title": "Accepted", - "description": "HTTP Requests successfully reporting new events", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 2], - [1535723940000, 0] - ] - }], - "apm_responses_errors": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.toolarge", - "metricAgg": "max", - "label": "Too large", - "title": "Response Errors Intake API", - "description": "HTTP Requests rejected due to excessive payload size", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.validate", - "metricAgg": "max", - "label": "Validate", - "title": "Validate", - "description": "HTTP Requests rejected due to payload validation error", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.method", - "metricAgg": "max", - "label": "Method", - "title": "Method", - "description": "HTTP Requests rejected due to incorrect HTTP method", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.unauthorized", - "metricAgg": "max", - "label": "Unauthorized", - "title": "Unauthorized", - "description": "HTTP Requests rejected due to invalid secret token", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.ratelimit", - "metricAgg": "max", - "label": "Rate limit", - "title": "Rate limit", - "description": "HTTP Requests rejected to due excessive rate limit", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.queue", - "metricAgg": "max", - "label": "Queue", - "title": "Queue", - "description": "HTTP Requests rejected to due internal queue filling up", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.decode", - "metricAgg": "max", - "label": "Decode", - "title": "Decode", - "description": "HTTP Requests rejected to due decoding errors - invalid json, incorrect data type for entity", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.forbidden", - "metricAgg": "max", - "label": "Forbidden", - "title": "Forbidden", - "description": "Forbidden HTTP Requests rejected - CORS violation, disabled enpoint", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.concurrency", - "metricAgg": "max", - "label": "Concurrency", - "title": "Concurrency", - "description": "HTTP Requests rejected due to overall concurrency limit breach", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.closed", - "metricAgg": "max", - "label": "Closed", - "title": "Closed", - "description": "HTTP Requests rejected during server shutdown", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.response.errors.internal", - "metricAgg": "max", - "label": "Internal", - "title": "Internal", - "description": "HTTP Requests rejected due to a miscellaneous internal error", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }], - "apm_requests": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.server.request.count", - "metricAgg": "max", - "label": "Requested", - "title": "Request Count Intake API", - "description": "HTTP Requests received by server", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 42], - [1535723940000, 42] - ] - }], - "apm_transformations": [{ - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.processor.transaction.transformations", - "metricAgg": "max", - "label": "Transaction", - "title": "Processed Events", - "description": "Transaction events processed", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 2], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.processor.span.transformations", - "metricAgg": "max", - "label": "Span", - "title": "Transformations", - "description": "Span events processed", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 4], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.processor.error.transformations", - "metricAgg": "max", - "label": "Error", - "title": "Transformations", - "description": "Error events processed", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }, { - "bucket_size": "30 seconds", - "timeRange": { - "min": 1535720389104, - "max": 1535723989104 - }, - "metric": { - "app": "apm", - "field": "beats_stats.metrics.apm-server.processor.metric.transformations", - "metricAgg": "max", - "label": "Metric", - "title": "Transformations", - "description": "Metric events processed", - "units": "/m", - "format": "0,0.[00]", - "hasCalculation": false, - "isDerivative": true - }, - "data": [ - [1535723880000, null], - [1535723910000, 0], - [1535723940000, 0] - ] - }] + "apm_cpu": [ + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.beat.cpu.total.value", + "metricAgg": "max", + "label": "Total", + "title": "CPU Utilization", + "description": "Percentage of CPU time spent executing (user+kernel mode) for the APM process", + "units": "%", + "format": "0.[00]", + "hasCalculation": true, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0.13333333333333333], + [1535723940000, 0.3] + ] + } + ], + "apm_os_load": [ + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.system.load.1", + "metricAgg": "max", + "label": "1m", + "title": "System Load", + "description": "Load average over the last 1 minute", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1535723880000, 1.37], + [1535723910000, 1.01], + [1535723940000, 0.61] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.system.load.5", + "metricAgg": "max", + "label": "5m", + "title": "System Load", + "description": "Load average over the last 5 minutes", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1535723880000, 1.72], + [1535723910000, 1.6], + [1535723940000, 1.45] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.system.load.15", + "metricAgg": "max", + "label": "15m", + "title": "System Load", + "description": "Load average over the last 15 minutes", + "units": "", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1535723880000, 2.5], + [1535723910000, 2.43], + [1535723940000, 2.35] + ] + } + ], + "apm_memory": [ + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.beat.memstats.memory_alloc", + "metricAgg": "max", + "label": "Allocated Memory", + "title": "Memory", + "description": "Allocated memory", + "units": "B", + "format": "0,0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1535723880000, 4421336], + [1535723910000, 3888048], + [1535723940000, 3087640] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.beat.memstats.rss", + "metricAgg": "max", + "label": "Process Total", + "title": "Memory", + "description": "Resident set size of memory reserved by the APM service from the OS", + "units": "B", + "format": "0,0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1535723880000, 10260480], + [1535723910000, 11456512], + [1535723940000, 12095488] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.beat.memstats.gc_next", + "metricAgg": "max", + "label": "GC Next", + "title": "Memory", + "description": "Limit of allocated memory at which garbage collection will occur", + "units": "B", + "format": "0,0.0 b", + "hasCalculation": false, + "isDerivative": false + }, + "data": [ + [1535723880000, 4996912], + [1535723910000, 4996912], + [1535723940000, 4886176] + ] + } + ], + "apm_output_events_rate_success": [ + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.libbeat.output.events.total", + "metricAgg": "max", + "label": "Total", + "title": "Output Events Rate", + "description": "Events processed by the output (including retries)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0.1], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.libbeat.output.events.active", + "metricAgg": "max", + "label": "Active", + "title": "Output Active Events Rate", + "description": "Events processed by the output (including retries)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.libbeat.output.events.acked", + "metricAgg": "max", + "label": "Acked", + "title": "Output Acked Events Rate", + "description": "Events processed by the output (including retries)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0.1], + [1535723940000, 0] + ] + } + ], + "apm_output_events_rate_failure": [ + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.libbeat.output.events.failed", + "metricAgg": "max", + "label": "Failed", + "title": "Output Failed Events Rate", + "description": "Events processed by the output (including retries)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.libbeat.output.events.dropped", + "metricAgg": "max", + "label": "Dropped", + "title": "Output Dropped Events Rate", + "description": "Events processed by the output (including retries)", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + } + ], + "apm_responses_valid": [ + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.count", + "metricAgg": "max", + "label": "Total", + "title": "Response Count Intake API", + "description": "HTTP Requests responded to by server", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0.7], + [1535723940000, 0.7] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.valid.ok", + "metricAgg": "max", + "label": "Ok", + "title": "Ok", + "description": "200 OK response count", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0.6666666666666666], + [1535723940000, 0.7] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.valid.accepted", + "metricAgg": "max", + "label": "Accepted", + "title": "Accepted", + "description": "HTTP Requests successfully reporting new events", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0.03333333333333333], + [1535723940000, 0] + ] + } + ], + "apm_responses_errors": [ + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.toolarge", + "metricAgg": "max", + "label": "Too large", + "title": "Response Errors Intake API", + "description": "HTTP Requests rejected due to excessive payload size", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.validate", + "metricAgg": "max", + "label": "Validate", + "title": "Validate", + "description": "HTTP Requests rejected due to payload validation error", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.method", + "metricAgg": "max", + "label": "Method", + "title": "Method", + "description": "HTTP Requests rejected due to incorrect HTTP method", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.unauthorized", + "metricAgg": "max", + "label": "Unauthorized", + "title": "Unauthorized", + "description": "HTTP Requests rejected due to invalid secret token", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.ratelimit", + "metricAgg": "max", + "label": "Rate limit", + "title": "Rate limit", + "description": "HTTP Requests rejected to due excessive rate limit", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.queue", + "metricAgg": "max", + "label": "Queue", + "title": "Queue", + "description": "HTTP Requests rejected to due internal queue filling up", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.decode", + "metricAgg": "max", + "label": "Decode", + "title": "Decode", + "description": "HTTP Requests rejected to due decoding errors - invalid json, incorrect data type for entity", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.forbidden", + "metricAgg": "max", + "label": "Forbidden", + "title": "Forbidden", + "description": "Forbidden HTTP Requests rejected - CORS violation, disabled enpoint", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.concurrency", + "metricAgg": "max", + "label": "Concurrency", + "title": "Concurrency", + "description": "HTTP Requests rejected due to overall concurrency limit breach", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.closed", + "metricAgg": "max", + "label": "Closed", + "title": "Closed", + "description": "HTTP Requests rejected during server shutdown", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.response.errors.internal", + "metricAgg": "max", + "label": "Internal", + "title": "Internal", + "description": "HTTP Requests rejected due to a miscellaneous internal error", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + } + ], + "apm_requests": [ + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.server.request.count", + "metricAgg": "max", + "label": "Requested", + "title": "Request Count Intake API", + "description": "HTTP Requests received by server", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0.7], + [1535723940000, 0.7] + ] + } + ], + "apm_transformations": [ + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.processor.transaction.transformations", + "metricAgg": "max", + "label": "Transaction", + "title": "Processed Events", + "description": "Transaction events processed", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0.03333333333333333], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.processor.span.transformations", + "metricAgg": "max", + "label": "Span", + "title": "Transformations", + "description": "Span events processed", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0.06666666666666667], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.processor.error.transformations", + "metricAgg": "max", + "label": "Error", + "title": "Transformations", + "description": "Error events processed", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.processor.metric.transformations", + "metricAgg": "max", + "label": "Metric", + "title": "Transformations", + "description": "Metric events processed", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + } + ], + "apm_acm_response": [ + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.acm.response.count", + "metricAgg": "max", + "label": "Count", + "title": "Response Count Agent Configuration Management", + "description": "HTTP requests responded to by APM Server", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.acm.response.errors.count", + "metricAgg": "max", + "label": "Error Count", + "title": "Response Error Count Agent Configuration Management", + "description": "HTTP errors count", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.acm.response.valid.ok", + "metricAgg": "max", + "label": "OK", + "title": "Response OK Count Agent Configuration Management", + "description": "200 OK response count", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.acm.response.valid.notmodified", + "metricAgg": "max", + "label": "Not Modified", + "title": "Response Not Modified Agent Configuration Management", + "description": "304 Not modified response count", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + } + ], + "apm_acm_response_errors": [ + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.acm.response.errors.forbidden", + "metricAgg": "max", + "label": "Count", + "title": "Response Errors Agent Configuration Management", + "description": "Forbidden HTTP requests rejected count", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.acm.response.errors.unauthorized", + "metricAgg": "max", + "label": "Unauthorized", + "title": "Response Unauthorized Errors Agent Configuration Management", + "description": "Unauthorized HTTP requests rejected count", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.acm.response.errors.unavailable", + "metricAgg": "max", + "label": "Unavailable", + "title": "Response Unavailable Errors Agent Configuration Management", + "description": "Unavailable HTTP response count. Possible misconfiguration or unsupported version of Kibana", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.acm.response.errors.method", + "metricAgg": "max", + "label": "Method", + "title": "Response Method Errors Agent Configuration Management", + "description": "HTTP requests rejected due to incorrect HTTP method", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + }, + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.acm.response.errors.invalidquery", + "metricAgg": "max", + "label": "Invalid Query", + "title": "Response Invalid Query Errors Agent Configuration Management", + "description": "Invalid HTTP query", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + } + ], + "apm_acm_request_count": [ + { + "bucket_size": "30 seconds", + "timeRange": { "min": 1535720389104, "max": 1535723989104 }, + "metric": { + "app": "apm", + "field": "beats_stats.metrics.apm-server.acm.request.count", + "metricAgg": "max", + "label": "Count", + "title": "Requests Agent Configuration Management", + "description": "HTTP Requests received by agent configuration managemen", + "units": "/s", + "format": "0,0.[00]", + "hasCalculation": false, + "isDerivative": true + }, + "data": [ + [1535723880000, null], + [1535723910000, 0], + [1535723940000, 0] + ] + } + ] }, "apmSummary": { "uuid": "9b16f434-2092-4983-a401-80a2b61c79d6", From aec4ca9ed3a013bf7d5f5af93fbc3c741c43857e Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Thu, 25 Jun 2020 12:58:44 -0400 Subject: [PATCH 42/82] [SECURITY SOLUTION][INGEST] Task/endpoint list tests (#69419) (#69951) endpoint func tests for endpoint details to ingest, edit datasource to policy, bug fix for security link --- .../edit_datasource_page/index.tsx | 2 +- .../sections/fleet/agent_list_page/index.tsx | 1 + .../configure_datasource.tsx | 8 +- x-pack/test/api_integration/services/index.ts | 2 +- x-pack/test/common/services/index.ts | 2 + .../services/ingest_manager.ts | 0 .../apps/endpoint/endpoint_list.ts | 84 ++++++++++--------- .../apps/endpoint/policy_details.ts | 41 ++++++++- .../ingest_manager_create_datasource_page.ts | 22 ++++- .../services/index.ts | 2 + 10 files changed, 119 insertions(+), 45 deletions(-) rename x-pack/test/{api_integration => common}/services/ingest_manager.ts (100%) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx index d47eea80da8b7..af39cb87f18c9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx @@ -242,7 +242,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { }; return ( - + {isLoadingData ? ( ) : loadingError || !agentConfig || !packageInfo ? ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 281a8d3a9745c..75d0556755149 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -489,6 +489,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { className="fleet__agentList__table" + data-test-subj="fleetAgentListTable" loading={isLoading && agentsRequest.isInitialRequest} hasActions={true} noItemsMessage={ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx index 20346cb720acb..7b4dc36def133 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx @@ -35,9 +35,13 @@ export const ConfigureEndpointDatasource = memo {from === 'edit' ? ( { const pageObjects = getPageObjects(['common', 'endpoint', 'header', 'endpointPageUtils']); @@ -17,11 +18,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { this.tags('ciGroup7'); const sleep = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms)); before(async () => { - await esArchiver.load('endpoint/metadata/api_feature'); + await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }); await pageObjects.endpoint.navigateToEndpointList(); }); - it('finds title', async () => { + it('finds page title', async () => { const title = await testSubjects.getVisibleText('pageViewHeaderLeftTitle'); expect(title).to.equal('Endpoints'); }); @@ -77,54 +78,61 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(tableData).to.eql(expectedData); }); - it('no details flyout when endpoint page displayed', async () => { + it('does not show the details flyout initially', async () => { await testSubjects.missingOrFail('hostDetailsFlyout'); }); - it('display details flyout when the hostname is clicked on', async () => { - await (await testSubjects.find('hostnameCellLink')).click(); - await testSubjects.existOrFail('hostDetailsUpperList'); - await testSubjects.existOrFail('hostDetailsLowerList'); - }); + describe('when the hostname is clicked on,', () => { + it('display the details flyout', async () => { + await (await testSubjects.find('hostnameCellLink')).click(); + await testSubjects.existOrFail('hostDetailsUpperList'); + await testSubjects.existOrFail('hostDetailsLowerList'); + }); - it('update details flyout when new hostname is clicked on', async () => { - // display flyout for the first host in the list - await (await testSubjects.findAll('hostnameCellLink'))[0].click(); - await testSubjects.existOrFail('hostDetailsFlyoutTitle'); - const hostDetailTitle0 = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); - // select the 2nd host in the host list - await (await testSubjects.findAll('hostnameCellLink'))[1].click(); - await pageObjects.endpoint.waitForVisibleTextToChange( - 'hostDetailsFlyoutTitle', - hostDetailTitle0 - ); - const hostDetailTitle1 = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); - expect(hostDetailTitle1).to.not.eql(hostDetailTitle0); - }); + it('updates the details flyout when a new hostname is selected from the list', async () => { + // display flyout for the first host in the list + await (await testSubjects.findAll('hostnameCellLink'))[0].click(); + await testSubjects.existOrFail('hostDetailsFlyoutTitle'); + const hostDetailTitle0 = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); + // select the 2nd host in the host list + await (await testSubjects.findAll('hostnameCellLink'))[1].click(); + await pageObjects.endpoint.waitForVisibleTextToChange( + 'hostDetailsFlyoutTitle', + hostDetailTitle0 + ); + const hostDetailTitle1 = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); + expect(hostDetailTitle1).to.not.eql(hostDetailTitle0); + }); + + it('has the same flyout info when the same hostname is selected', async () => { + // display flyout for the first host in the list + await (await testSubjects.findAll('hostnameCellLink'))[1].click(); + await testSubjects.existOrFail('hostDetailsFlyoutTitle'); + const hostDetailTitleInitial = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); + // select the same host in the host list + await (await testSubjects.findAll('hostnameCellLink'))[1].click(); + await sleep(500); // give page time to refresh and verify it did not change + const hostDetailTitleNew = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); + expect(hostDetailTitleNew).to.equal(hostDetailTitleInitial); + }); - it('details flyout remains the same when current hostname is clicked on', async () => { - // display flyout for the first host in the list - await (await testSubjects.findAll('hostnameCellLink'))[1].click(); - await testSubjects.existOrFail('hostDetailsFlyoutTitle'); - const hostDetailTitleInitial = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); - // select the same host in the host list - await (await testSubjects.findAll('hostnameCellLink'))[1].click(); - await sleep(500); // give page time to refresh and verify it did not change - const hostDetailTitleNew = await testSubjects.getVisibleText('hostDetailsFlyoutTitle'); - expect(hostDetailTitleNew).to.equal(hostDetailTitleInitial); + it('navigates to ingest fleet when the Reassign Policy link is clicked', async () => { + await (await testSubjects.find('hostDetailsLinkToIngest')).click(); + await testSubjects.existOrFail('fleetAgentListTable'); + }); }); - describe('no data', () => { + describe('when there is no data,', () => { before(async () => { // clear out the data and reload the page - await esArchiver.unload('endpoint/metadata/api_feature'); + await deleteMetadataStream(getService); await pageObjects.endpoint.navigateToEndpointList(); }); after(async () => { // reload the data so the other tests continue to pass - await esArchiver.load('endpoint/metadata/api_feature'); + await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }); }); - it('displays no items found when empty', async () => { + it('displays No items found when empty', async () => { // get the endpoint list table data and verify message const [, [noItemsFoundMessage]] = await pageObjects.endpointPageUtils.tableData( 'hostListTable' @@ -166,7 +174,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Windows 10', '', '0', - '00000000-0000-0000-0000-000000000000', + 'Default', 'Unknown', '10.101.149.262606:a000:ffc0:39:11ef:37b9:3371:578c', 'rezzani-7.example.com', @@ -175,7 +183,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); after(async () => { - await esArchiver.unload('endpoint/metadata/api_feature'); + await deleteMetadataStream(getService); }); }); }; diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 036f82a591fb3..b0c161ca1d0c2 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -9,7 +9,13 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { PolicyTestResourceInfo } from '../../services/endpoint_policy'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - const pageObjects = getPageObjects(['common', 'endpoint', 'policy', 'endpointPageUtils']); + const pageObjects = getPageObjects([ + 'common', + 'endpoint', + 'policy', + 'endpointPageUtils', + 'ingestManagerCreateDatasource', + ]); const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); @@ -185,5 +191,38 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); }); + + describe('when on Ingest Configurations Edit Datasource page', async () => { + let policyInfo: PolicyTestResourceInfo; + beforeEach(async () => { + // Create a policy and navigate to Ingest app + policyInfo = await policyTestResources.createPolicy(); + await pageObjects.ingestManagerCreateDatasource.navigateToAgentConfigEditDatasource( + policyInfo.agentConfig.id, + policyInfo.datasource.id + ); + }); + afterEach(async () => { + if (policyInfo) { + await policyInfo.cleanup(); + } + }); + it('should show a link to Policy Details', async () => { + await testSubjects.existOrFail('editLinkToPolicyDetails'); + }); + it('should navigate to Policy Details when the link is clicked', async () => { + const linkToPolicy = await testSubjects.find('editLinkToPolicyDetails'); + await linkToPolicy.click(); + await pageObjects.policy.ensureIsOnDetailsPage(); + }); + it('should allow the user to navigate, edit and save Policy Details', async () => { + await (await testSubjects.find('editLinkToPolicyDetails')).click(); + await pageObjects.policy.ensureIsOnDetailsPage(); + await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); + await pageObjects.policy.confirmAndSave(); + + await testSubjects.existOrFail('policyDetailsSuccessMessage'); + }); + }); }); } diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts index f50cde6285be7..e104b8701276c 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts @@ -6,13 +6,14 @@ import { FtrProviderContext } from '../ftr_provider_context'; -export function IngestManagerCreateDatasource({ getService }: FtrProviderContext) { +export function IngestManagerCreateDatasource({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); + const pageObjects = getPageObjects(['common']); return { /** - * Validates that the page shown is the Datasource Craete Page + * Validates that the page shown is the Datasource Create Page */ async ensureOnCreatePageOrFail() { await testSubjects.existOrFail('createDataSource_header'); @@ -75,5 +76,22 @@ export function IngestManagerCreateDatasource({ getService }: FtrProviderContext async waitForSaveSuccessNotification() { await testSubjects.existOrFail('datasourceCreateSuccessToast'); }, + + /** + * Validates that the page shown is the Datasource Edit Page + */ + async ensureOnEditPageOrFail() { + await testSubjects.existOrFail('editDataSource_header'); + }, + + /** + * Navigates to the Ingest Agent configuration Edit Datasource page + */ + async navigateToAgentConfigEditDatasource(agentConfigId: string, datasourceId: string) { + await pageObjects.common.navigateToApp('ingestManager', { + hash: `/configs/${agentConfigId}/edit-datasource/${datasourceId}`, + }); + await this.ensureOnEditPageOrFail(); + }, }; } diff --git a/x-pack/test/security_solution_endpoint/services/index.ts b/x-pack/test/security_solution_endpoint/services/index.ts index 0247d9b00968a..7eecae41aae4a 100644 --- a/x-pack/test/security_solution_endpoint/services/index.ts +++ b/x-pack/test/security_solution_endpoint/services/index.ts @@ -6,8 +6,10 @@ import { services as xPackFunctionalServices } from '../../functional/services'; import { EndpointPolicyTestResourcesProvider } from './endpoint_policy'; +import { IngestManagerProvider } from '../../common/services/ingest_manager'; export const services = { ...xPackFunctionalServices, policyTestResources: EndpointPolicyTestResourcesProvider, + ingestManager: IngestManagerProvider, }; From dd1d3dd0d254ec8c2e175c3dd9faa28b0b13d81a Mon Sep 17 00:00:00 2001 From: igoristic Date: Thu, 25 Jun 2020 13:27:56 -0400 Subject: [PATCH 43/82] Fixed links missing a hash (#69861) (#69877) Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../monitoring/public/components/logstash/listing/listing.js | 2 +- .../components/logstash/pipeline_listing/pipeline_listing.js | 2 +- .../public/directives/elasticsearch/ml_job_listing/index.js | 4 +++- x-pack/plugins/monitoring/public/directives/main/index.js | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js index b22cbfbc84de7..18158f8838edd 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js @@ -54,7 +54,7 @@ export class Listing extends PureComponent { return (
- + {name}
diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js index af181fb2a674d..10e16ce61feef 100644 --- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js @@ -44,7 +44,7 @@ export class PipelineListing extends Component { field: 'id', sortable: true, render: (id) => ( - + {id} ), diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js index bef0fce4cd088..ec325673ddfda 100644 --- a/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js @@ -72,7 +72,9 @@ const getColumns = () => [ render: (name, node) => { if (node) { return ( - {name} + + {name} + ); } diff --git a/x-pack/plugins/monitoring/public/directives/main/index.js b/x-pack/plugins/monitoring/public/directives/main/index.js index 97ec66c9b3415..eda32cd39c0d0 100644 --- a/x-pack/plugins/monitoring/public/directives/main/index.js +++ b/x-pack/plugins/monitoring/public/directives/main/index.js @@ -133,7 +133,7 @@ export class MonitoringMainController { this.pipelineHashShort = shortenPipelineHash(this.pipelineHash); this.onChangePipelineHash = () => { window.location.hash = getSafeForExternalLink( - `/logstash/pipelines/${this.pipelineId}/${this.pipelineHash}` + `#/logstash/pipelines/${this.pipelineId}/${this.pipelineHash}` ); }; } From 4a525df06148109f6330913d8cc4aa3f01b3e438 Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Thu, 25 Jun 2020 13:33:13 -0400 Subject: [PATCH 44/82] [Endpoint]EMT-451: add ability to filter endpoint metadata based on presence of unenrolled events (#69708) (#69971) [Endpoint]EMT-451: add ability to filter endpoint metadata based on presence of unenrolled events --- .../common/endpoint/constants.ts | 1 + .../common/endpoint/generate_data.ts | 5 +- .../common/endpoint/types.ts | 19 +- .../server/endpoint/routes/metadata/index.ts | 24 ++- .../endpoint/routes/metadata/metadata.test.ts | 134 ++++++++++---- .../routes/metadata/query_builders.test.ts | 175 +++++++++++++++++- .../routes/metadata/query_builders.ts | 66 +++++-- .../routes/metadata/support/unenroll.test.ts | 147 +++++++++++++++ .../routes/metadata/support/unenroll.ts | 114 ++++++++++++ .../apis/endpoint/data_stream_helper.ts | 5 + .../api_integration/apis/endpoint/metadata.ts | 36 +++- .../unenroll_feature/metadata/data.json.gz | Bin 0 -> 598 bytes .../metadata_mirror/data.json.gz | Bin 0 -> 535 bytes 13 files changed, 670 insertions(+), 56 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts create mode 100644 x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata/data.json.gz create mode 100644 x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata_mirror/data.json.gz diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index e311e358e6146..984cd7d2506a9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -7,5 +7,6 @@ export const eventsIndexPattern = 'logs-endpoint.events.*'; export const alertsIndexPattern = 'logs-endpoint.alerts-*'; export const metadataIndexPattern = 'metrics-endpoint.metadata-*'; +export const metadataMirrorIndexPattern = 'metrics-endpoint.metadata_mirror-*'; export const policyIndexPattern = 'metrics-endpoint.policy-*'; export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*'; diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index ef9e8376827a0..5af34b6a694e8 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -14,6 +14,7 @@ import { HostPolicyResponse, HostPolicyResponseActionStatus, PolicyData, + EndpointStatus, } from './types'; import { factory as policyFactory } from './models/policy_config'; @@ -209,6 +210,7 @@ interface HostInfo { }; host: Host; Endpoint: { + status: EndpointStatus; policy: { applied: { id: string; @@ -305,7 +307,7 @@ export class EndpointDocGenerator { * Creates new random policy id for the host to simulate new policy application */ public updatePolicyId() { - this.commonInfo.Endpoint.policy.applied = this.randomChoice(APPLIED_POLICIES); + this.commonInfo.Endpoint.policy.applied.id = this.randomChoice(APPLIED_POLICIES).id; this.commonInfo.Endpoint.policy.applied.status = this.randomChoice([ HostPolicyResponseActionStatus.success, HostPolicyResponseActionStatus.failure, @@ -333,6 +335,7 @@ export class EndpointDocGenerator { os: this.randomChoice(OS), }, Endpoint: { + status: EndpointStatus.enrolled, policy: { applied: this.randomChoice(APPLIED_POLICIES), }, diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index f8cfb8f7c3bbc..4f13fd97ce442 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -350,7 +350,23 @@ export interface AlertEvent { } /** - * The status of the host + * The status of the Endpoint Agent as reported by the Agent or the + * Security Solution app using events from Fleet. + */ +export enum EndpointStatus { + /** + * Agent is enrolled with Fleet + */ + enrolled = 'enrolled', + + /** + * Agent is unenrrolled from Fleet + */ + unenrolled = 'unenrolled', +} + +/** + * The status of the host, which is mapped to the Elastic Agent status in Fleet */ export enum HostStatus { /** @@ -386,6 +402,7 @@ export type HostMetadata = Immutable<{ }; }; Endpoint: { + status: EndpointStatus; policy: { applied: { id: string; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 4037f1a7cbc46..7c50a10846f9a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -8,6 +8,7 @@ import { IRouter, Logger, RequestHandlerContext } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; +import Boom from 'boom'; import { metadataIndexPattern } from '../../../../common/endpoint/constants'; import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; import { @@ -18,6 +19,7 @@ import { } from '../../../../common/endpoint/types'; import { EndpointAppContext } from '../../types'; import { AgentStatus } from '../../../../../ingest_manager/common/types/models'; +import { findAllUnenrolledHostIds, findUnenrolledHostByHostId, HostId } from './support/unenroll'; interface HitSource { _source: HostMetadata; @@ -68,10 +70,17 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { + const unenrolledHostIds = await findAllUnenrolledHostIds( + context.core.elasticsearch.legacy.client + ); + const queryParams = await kibanaRequestToMetadataListESQuery( req, endpointAppContext, - metadataIndexPattern + metadataIndexPattern, + { + unenrolledHostIds: unenrolledHostIds.map((host: HostId) => host.host.id), + } ); const response = (await context.core.elasticsearch.legacy.client.callAsCurrentUser( 'search', @@ -113,6 +122,12 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp return res.notFound({ body: 'Endpoint Not Found' }); } catch (err) { logger.warn(JSON.stringify(err, null, 2)); + if (err.isBoom) { + return res.customError({ + statusCode: err.output.statusCode, + body: { message: err.message }, + }); + } return res.internalError({ body: err }); } } @@ -123,6 +138,13 @@ export async function getHostData( metadataRequestContext: MetadataRequestContext, id: string ): Promise { + const unenrolledHostId = await findUnenrolledHostByHostId( + metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client, + id + ); + if (unenrolledHostId) { + throw Boom.badRequest('the requested endpoint is unenrolled'); + } const query = getESQueryHostMetadataByID(id, metadataIndexPattern); const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client.callAsCurrentUser( 'search', diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index c04975fa8b28e..1ca205f669fa3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -35,6 +35,7 @@ import Boom from 'boom'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { HostId } from './support/unenroll'; describe('test endpoint route', () => { let routerMock: jest.Mocked; @@ -50,6 +51,12 @@ describe('test endpoint route', () => { typeof createMockEndpointAppContextServiceStartContract >['agentService']; let endpointAppContextService: EndpointAppContextService; + const noUnenrolledEndpoint = () => + Promise.resolve(({ + hits: { + hits: [], + }, + } as unknown) as SearchResponse); beforeEach(() => { mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked< @@ -77,7 +84,9 @@ describe('test endpoint route', () => { it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); const response = createSearchResponse(new EndpointDocGenerator().generateHostMetadata()); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -88,7 +97,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; @@ -113,9 +122,11 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) - ); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => + Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) + ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -126,8 +137,8 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); - expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query).toEqual({ + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(mockScopedClient.callAsCurrentUser.mock.calls[1][1]?.body?.query).toEqual({ match_all: {}, }); expect(routeConfig.options).toEqual({ authRequired: true }); @@ -156,9 +167,11 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) - ); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => + Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) + ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -170,20 +183,26 @@ describe('test endpoint route', () => { ); expect(mockScopedClient.callAsCurrentUser).toBeCalled(); - expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query).toEqual({ + expect(mockScopedClient.callAsCurrentUser.mock.calls[1][1]?.body?.query).toEqual({ bool: { - must_not: { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'host.ip': '10.140.73.246', + must: [ + { + bool: { + must_not: { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'host.ip': '10.140.73.246', + }, + }, + ], }, }, - ], + }, }, - }, + ], }, }); expect(routeConfig.options).toEqual({ authRequired: true }); @@ -199,9 +218,10 @@ describe('test endpoint route', () => { it('should return 404 on no results', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createSearchResponse()) - ); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => Promise.resolve(createSearchResponse())); + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -212,7 +232,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.notFound).toBeCalled(); const message = mockResponse.notFound.mock.calls[0][0]?.body; @@ -224,8 +244,12 @@ describe('test endpoint route', () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: response.hits.hits[0]._id }, }); + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online'); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -236,7 +260,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; @@ -254,7 +278,11 @@ describe('test endpoint route', () => { mockAgentService.getAgentStatusById = jest.fn().mockImplementation(() => { throw Boom.notFound('Agent not found'); }); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -265,7 +293,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; @@ -280,7 +308,11 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning'); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(noUnenrolledEndpoint) + .mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -291,12 +323,50 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result.host_status).toEqual(HostStatus.ERROR); }); + + it('should throw error when endpoint is unenrolled', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ + params: { id: 'hostId' }, + }); + + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(({ + hits: { + hits: [ + { + _index: 'metrics-endpoint.metadata_mirror-default', + _id: 'S5M1yHIBLSMVtiLw6Wpr', + _score: 0.0, + _source: { + host: { + id: 'hostId', + }, + }, + }, + ], + }, + } as unknown) as SearchResponse) + ); + + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/metadata') + )!; + + await routeHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockResponse.customError).toBeCalled(); + }); }); }); @@ -319,7 +389,7 @@ function createSearchResponse(hostMetadata?: HostMetadata): SearchResponse { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as Record); }); + + it( + 'test default query params for all endpoints metadata when no params or body is provided ' + + 'with unenrolled host ids excluded', + async () => { + const unenrolledHostId = '1fdca33f-799f-49f4-939c-ea4383c77672'; + const mockRequest = httpServerMock.createKibanaRequest({ + body: {}, + }); + const query = await kibanaRequestToMetadataListESQuery( + mockRequest, + { + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), + }, + metadataIndexPattern, + { + unenrolledHostIds: [unenrolledHostId], + } + ); + + expect(query).toEqual({ + body: { + query: { + bool: { + must_not: { + terms: { + 'host.id': ['1fdca33f-799f-49f4-939c-ea4383c77672'], + }, + }, + }, + }, + collapse: { + field: 'host.id', + inner_hits: { + name: 'most_recent', + size: 1, + sort: [{ 'event.created': 'desc' }], + }, + }, + aggs: { + total: { + cardinality: { + field: 'host.id', + }, + }, + }, + sort: [ + { + 'event.created': { + order: 'desc', + }, + }, + ], + }, + from: 0, + size: 10, + index: metadataIndexPattern, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as Record); + } + ); }); describe('test query builder with kql filter', () => { @@ -76,22 +139,29 @@ describe('query builder', () => { }, metadataIndexPattern ); + expect(query).toEqual({ body: { query: { bool: { - must_not: { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'host.ip': '10.140.73.246', + must: [ + { + bool: { + must_not: { + bool: { + should: [ + { + match: { + 'host.ip': '10.140.73.246', + }, + }, + ], + minimum_should_match: 1, }, }, - ], + }, }, - }, + ], }, }, collapse: { @@ -123,6 +193,93 @@ describe('query builder', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } as Record); }); + + it( + 'test default query params for all endpoints endpoint metadata excluding unerolled endpoint ' + + 'and when body filter is provided', + async () => { + const unenrolledHostId = '1fdca33f-799f-49f4-939c-ea4383c77672'; + const mockRequest = httpServerMock.createKibanaRequest({ + body: { + filter: 'not host.ip:10.140.73.246', + }, + }); + const query = await kibanaRequestToMetadataListESQuery( + mockRequest, + { + logFactory: loggingSystemMock.create(), + service: new EndpointAppContextService(), + config: () => Promise.resolve(createMockConfig()), + }, + metadataIndexPattern, + { + unenrolledHostIds: [unenrolledHostId], + } + ); + + expect(query).toEqual({ + body: { + query: { + bool: { + must: [ + { + bool: { + must_not: { + terms: { + 'host.id': [unenrolledHostId], + }, + }, + }, + }, + { + bool: { + must_not: { + bool: { + should: [ + { + match: { + 'host.ip': '10.140.73.246', + }, + }, + ], + minimum_should_match: 1, + }, + }, + }, + }, + ], + }, + }, + collapse: { + field: 'host.id', + inner_hits: { + name: 'most_recent', + size: 1, + sort: [{ 'event.created': 'desc' }], + }, + }, + aggs: { + total: { + cardinality: { + field: 'host.id', + }, + }, + }, + sort: [ + { + 'event.created': { + order: 'desc', + }, + }, + ], + }, + from: 0, + size: 10, + index: metadataIndexPattern, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as Record); + } + ); }); describe('MetadataGetQuery', () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index 075e4377f0b2a..b6ec91675f248 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -7,17 +7,22 @@ import { KibanaRequest } from 'kibana/server'; import { esKuery } from '../../../../../../../src/plugins/data/server'; import { EndpointAppContext } from '../../types'; -export const kibanaRequestToMetadataListESQuery = async ( +export interface QueryBuilderOptions { + unenrolledHostIds?: string[]; +} + +export async function kibanaRequestToMetadataListESQuery( // eslint-disable-next-line @typescript-eslint/no-explicit-any request: KibanaRequest, endpointAppContext: EndpointAppContext, - index: string + index: string, + queryBuilderOptions?: QueryBuilderOptions // eslint-disable-next-line @typescript-eslint/no-explicit-any -): Promise> => { +): Promise> { const pagingProperties = await getPagingProperties(request, endpointAppContext); return { body: { - query: buildQueryBody(request), + query: buildQueryBody(request, queryBuilderOptions?.unenrolledHostIds!), collapse: { field: 'host.id', inner_hits: { @@ -45,7 +50,7 @@ export const kibanaRequestToMetadataListESQuery = async ( size: pagingProperties.pageSize, index, }; -}; +} async function getPagingProperties( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -68,14 +73,53 @@ async function getPagingProperties( }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function buildQueryBody(request: KibanaRequest): Record { +function buildQueryBody( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request: KibanaRequest, + unerolledHostIds: string[] | undefined + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): Record { + const filterUnenrolledHosts = unerolledHostIds && unerolledHostIds.length > 0; if (typeof request?.body?.filter === 'string') { - return esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(request.body.filter)); + const kqlQuery = esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(request.body.filter)); + return { + bool: { + must: filterUnenrolledHosts + ? [ + { + bool: { + must_not: { + terms: { + 'host.id': unerolledHostIds, + }, + }, + }, + }, + { + ...kqlQuery, + }, + ] + : [ + { + ...kqlQuery, + }, + ], + }, + }; } - return { - match_all: {}, - }; + return filterUnenrolledHosts + ? { + bool: { + must_not: { + terms: { + 'host.id': unerolledHostIds, + }, + }, + }, + } + : { + match_all: {}, + }; } export function getESQueryHostMetadataByID(hostID: string, index: string) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts new file mode 100644 index 0000000000000..2e6bb2c976fef --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IScopedClusterClient } from 'kibana/server'; +import { + findAllUnenrolledHostIds, + fetchAllUnenrolledHostIdsWithScroll, + HostId, + findUnenrolledHostByHostId, +} from './unenroll'; +import { elasticsearchServiceMock } from '../../../../../../../../src/core/server/mocks'; +import { SearchResponse } from 'elasticsearch'; +import { metadataMirrorIndexPattern } from '../../../../../common/endpoint/constants'; +import { EndpointStatus } from '../../../../../common/endpoint/types'; + +const noUnenrolledEndpoint = () => + Promise.resolve(({ + hits: { + hits: [], + }, + } as unknown) as SearchResponse); + +describe('test find all unenrolled HostId', () => { + let mockScopedClient: jest.Mocked; + + it('can find all hits with scroll', async () => { + const firstHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; + const secondHostId = '2fdca33f-799f-49f4-939c-ea4383c77672'; + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(() => Promise.resolve(createSearchResponse(secondHostId, 'scrollId'))) + .mockImplementationOnce(noUnenrolledEndpoint); + + const initialResponse = createSearchResponse(firstHostId, 'initialScrollId'); + const hostIds = await fetchAllUnenrolledHostIdsWithScroll( + initialResponse, + mockScopedClient.callAsCurrentUser + ); + + expect(hostIds).toEqual([{ host: { id: firstHostId } }, { host: { id: secondHostId } }]); + }); + + it('can find all unerolled endpoint host ids', async () => { + const firstEndpointHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; + const secondEndpointHostId = '2fdca33f-799f-49f4-939c-ea4383c77672'; + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClient.callAsCurrentUser + .mockImplementationOnce(() => + Promise.resolve(createSearchResponse(firstEndpointHostId, 'initialScrollId')) + ) + .mockImplementationOnce(() => + Promise.resolve(createSearchResponse(secondEndpointHostId, 'scrollId')) + ) + .mockImplementationOnce(noUnenrolledEndpoint); + const hostIds = await findAllUnenrolledHostIds(mockScopedClient); + + expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]).toEqual({ + index: metadataMirrorIndexPattern, + scroll: '30s', + body: { + size: 1000, + _source: ['host.id'], + query: { + bool: { + filter: { + term: { + 'Endpoint.status': EndpointStatus.unenrolled, + }, + }, + }, + }, + }, + }); + expect(hostIds).toEqual([ + { host: { id: firstEndpointHostId } }, + { host: { id: secondEndpointHostId } }, + ]); + }); +}); + +describe('test find unenrolled endpoint host id by hostId', () => { + let mockScopedClient: jest.Mocked; + + it('can find unenrolled endpoint by the host id when unenrolled', async () => { + const firstEndpointHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createSearchResponse(firstEndpointHostId, 'initialScrollId')) + ); + const endpointHostId = await findUnenrolledHostByHostId(mockScopedClient, firstEndpointHostId); + expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.index).toEqual( + metadataMirrorIndexPattern + ); + expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body).toEqual({ + size: 1, + _source: ['host.id'], + query: { + bool: { + filter: [ + { + term: { + 'Endpoint.status': EndpointStatus.unenrolled, + }, + }, + { + term: { + 'host.id': firstEndpointHostId, + }, + }, + ], + }, + }, + }); + expect(endpointHostId).toEqual({ host: { id: firstEndpointHostId } }); + }); + + it('find unenrolled endpoint host by the host id return undefined when no unenrolled host', async () => { + const firstHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(noUnenrolledEndpoint); + const hostId = await findUnenrolledHostByHostId(mockScopedClient, firstHostId); + expect(hostId).toBeFalsy(); + }); +}); + +function createSearchResponse(hostId: string, scrollId: string): SearchResponse { + return ({ + hits: { + hits: [ + { + _index: metadataMirrorIndexPattern, + _id: 'S5M1yHIBLSMVtiLw6Wpr', + _score: 0.0, + _source: { + host: { + id: hostId, + }, + }, + }, + ], + }, + _scroll_id: scrollId, + } as unknown) as SearchResponse; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts new file mode 100644 index 0000000000000..ef6898fad2807 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { APICaller, IScopedClusterClient } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; +import { metadataMirrorIndexPattern } from '../../../../../common/endpoint/constants'; +import { EndpointStatus } from '../../../../../common/endpoint/types'; + +const KEEPALIVE = '30s'; +const SIZE = 1000; + +export interface HostId { + host: { + id: string; + }; +} + +interface HitSource { + _source: HostId; +} + +export async function findUnenrolledHostByHostId( + client: IScopedClusterClient, + hostId: string +): Promise { + const queryParams = { + index: metadataMirrorIndexPattern, + body: { + size: 1, + _source: ['host.id'], + query: { + bool: { + filter: [ + { + term: { + 'Endpoint.status': EndpointStatus.unenrolled, + }, + }, + { + term: { + 'host.id': hostId, + }, + }, + ], + }, + }, + }, + }; + + const response = (await client.callAsCurrentUser('search', queryParams)) as SearchResponse< + HostId + >; + const newHits = response.hits?.hits || []; + + if (newHits.length > 0) { + const hostIds = newHits.map((hitSource: HitSource) => hitSource._source); + return hostIds[0]; + } else { + return undefined; + } +} + +export async function findAllUnenrolledHostIds(client: IScopedClusterClient): Promise { + const queryParams = { + index: metadataMirrorIndexPattern, + scroll: KEEPALIVE, + body: { + size: SIZE, + _source: ['host.id'], + query: { + bool: { + filter: { + term: { + 'Endpoint.status': EndpointStatus.unenrolled, + }, + }, + }, + }, + }, + }; + const response = (await client.callAsCurrentUser('search', queryParams)) as SearchResponse< + HostId + >; + + return fetchAllUnenrolledHostIdsWithScroll(response, client.callAsCurrentUser); +} + +export async function fetchAllUnenrolledHostIdsWithScroll( + response: SearchResponse, + client: APICaller, + hits: HostId[] = [] +): Promise { + let newHits = response.hits?.hits || []; + let scrollId = response._scroll_id; + + while (newHits.length > 0) { + const hostIds: HostId[] = newHits.map((hitSource: HitSource) => hitSource._source); + hits.push(...hostIds); + + const innerResponse = await client('scroll', { + body: { + scroll: KEEPALIVE, + scroll_id: scrollId, + }, + }); + + newHits = innerResponse.hits?.hits || []; + scrollId = innerResponse._scroll_id; + } + return hits; +} diff --git a/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts b/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts index b239ab41e41f1..d2e99a80ef8a1 100644 --- a/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts +++ b/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts @@ -10,6 +10,7 @@ import { eventsIndexPattern, alertsIndexPattern, policyIndexPattern, + metadataMirrorIndexPattern, } from '../../../../plugins/security_solution/common/endpoint/constants'; export async function deleteDataStream(getService: (serviceName: 'es') => Client, index: string) { @@ -29,6 +30,10 @@ export async function deleteMetadataStream(getService: (serviceName: 'es') => Cl await deleteDataStream(getService, metadataIndexPattern); } +export async function deleteMetadataMirrorStream(getService: (serviceName: 'es') => Client) { + await deleteDataStream(getService, metadataMirrorIndexPattern); +} + export async function deleteEventsStream(getService: (serviceName: 'es') => Client) { await deleteDataStream(getService, eventsIndexPattern); } diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index 41531269ddeb9..0d77486e07536 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { deleteMetadataStream } from './data_stream_helper'; +import { deleteMetadataMirrorStream, deleteMetadataStream } from './data_stream_helper'; /** * The number of host documents in the es archive. @@ -33,6 +33,40 @@ export default function ({ getService }: FtrProviderContext) { }); }); + describe('POST /api/endpoint/metadata when metadata mirror index contains unenrolled host', () => { + before(async () => { + await esArchiver.load('endpoint/metadata/unenroll_feature/metadata', { useCreate: true }); + await esArchiver.load('endpoint/metadata/unenroll_feature/metadata_mirror', { + useCreate: true, + }); + }); + + after(async () => { + await deleteMetadataStream(getService); + await deleteMetadataMirrorStream(getService); + }); + + it('metadata api should return only enrolled host', async () => { + const { body } = await supertest + .post('/api/endpoint/metadata') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + expect(body.total).to.eql(1); + expect(body.hosts.length).to.eql(1); + expect(body.request_page_size).to.eql(10); + expect(body.request_page_index).to.eql(0); + }); + + it('metadata api should return 400 when an unenrolled host is retrieved', async () => { + const { body } = await supertest + .get('/api/endpoint/metadata/1fdca33f-799f-49f4-939c-ea4383c77671') + .send() + .expect(400); + expect(body.message).to.eql('the requested endpoint is unenrolled'); + }); + }); + describe('POST /api/endpoint/metadata when index is not empty', () => { before( async () => await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }) diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata/data.json.gz b/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..d7b130e4051569b283b25ab9337bcc07cd4eeb16 GIT binary patch literal 598 zcmV-c0;&BUiwFpV`RrZ*17u-zVJ>QOZ*BnXR7sE1FciM$S5!IImbWbK35@_vkvK6D zFo=WWWw4r!EG;vm{&$>C(`KnOED{I62P^*Gd-nI1e2?B@;WziC_E!sE71CdJz*eMf zhdjE2J6hFQ4XHjpT(7Uf8}Mdl>D9wpzCO5j9=X!rI;TuGm6bKnxhe~rH_!n>iADgW zjcC&b;6A1<+De{Zamb6tX1Z=fRyq_1oG=o`h@v=L_AalE_YT4wS{A95_an@qqAXLZ z)dW7}gN_Sa*!tx!$C0_n4wZWOl+4uZxHoNmD3-8kTWNn_-=Dts=deMD&Z{C#9ba$a z<%>H#&G;z=91%m3C;~{(05BRvAPI2*bYfJX5sR3?1CFOg_uU!Vwz{fqk$2`05=iDW zbSmn`$}y2Sw=+8=IYUVT6pbZdA!0x%Vt^t9#Yje!hU8qJ{rtV{ENxk7(HvUp6GU9E zLV)=VrmFz0f*5knZs)we6!qkq4(VHY?Y@ECB|J;%MtmKX(WuC_o8C{ua$p1rLeO;!Vva{c)7dbElt4N+5XxX2LmcCCnLZC*%7mOg zl}KPBU^L(i&=9E0fki#-m}%3rOZL6{lZ#!wc&95j5DS7Z8Pn>^wmUkySs6QQMPeSn{{P16=M&>`QQ{;J_BL9L;u@#~#5<-ON k@8}fU-08Vak>_=a{De-CU)3C~{}R#p4wUF`<{SwC0D?arZ2$lO literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata_mirror/data.json.gz b/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata_mirror/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..3b4da7c47d9f22017c0b80365a9e613e90dbbf5e GIT binary patch literal 535 zcmV+y0_go8iwFpu`RrZ*17u-zVJ>QOZ*Bl>Q^{`IFc7`_D-4}80yi!6sR&vi>9qlh zwkQIMOG`vsDamdc_}@!9maWA+5aOFR;^e=F$NTgNJ|8T-|MqCQ6Fo3$rT+#}rF&;(2f9{mW9vTlfKZ|r&y{tqaiFvj zL)il!Q@dtx^7@!ZKJ>QIT`#KEqd4J&ku*mX<>}o>`E^lAla!&>wQI`KE8Z-4 zk@%&THNO{uGh#@QWq<@tfYBs_BE<>l!l*;Bzzi)#Whn)%?r!5#`;mGnjYnYQFyhEY;bY9Qm>0ON)Mr(A*- zjOJ8kS(?q7Y{UHin6?9>m>?8;w_?okY-~ad)0mQ&t^Ti-A-(}rDJ9&%TVlB|4TQAZu><$KM-4jFqz95+jck;{jAIhd*Q4&`L?)h ZL7R=+jWO7a`*CyJ{0IMZTXL`j004KW02=@R literal 0 HcmV?d00001 From 859871e51c660e99809baa06865fb3ea20f299d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 25 Jun 2020 19:37:58 +0200 Subject: [PATCH 45/82] [7.x][ML] Changes create DFA job page title. (#69943) --- .../data_frame_analytics/pages/analytics_management/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx index c0b7d63e623ce..07442124959d0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/page.tsx @@ -48,7 +48,7 @@ export const Page: FC = () => {

  Date: Thu, 25 Jun 2020 19:49:38 +0200 Subject: [PATCH 46/82] [SIEM] Replace WithSource with useWithSource hook (#68722) (#69974) --- .../detection_engine.test.tsx | 10 +- .../detection_engine/detection_engine.tsx | 142 ++++---- .../rules/details/index.test.tsx | 8 +- .../detection_engine/rules/details/index.tsx | 334 +++++++++--------- .../public/app/home/index.tsx | 38 +- .../draggable_wrapper_hover_content.test.tsx | 161 ++++----- .../draggable_wrapper_hover_content.tsx | 60 ++-- .../common/components/header_global/index.tsx | 96 +++-- .../public/common/components/top_n/index.tsx | 90 +++-- .../common/containers/global_time/index.tsx | 2 + .../common/containers/source/index.test.tsx | 67 ++-- .../public/common/containers/source/index.tsx | 169 ++++----- .../hosts/pages/details/details_tabs.test.tsx | 8 +- .../public/hosts/pages/details/index.tsx | 219 ++++++------ .../public/hosts/pages/hosts.test.tsx | 87 ++--- .../public/hosts/pages/hosts.tsx | 154 ++++---- .../__snapshots__/index.test.tsx.snap | 19 +- .../network/pages/ip_details/index.test.tsx | 41 +-- .../public/network/pages/ip_details/index.tsx | 304 ++++++++-------- .../public/network/pages/network.test.tsx | 69 ++-- .../public/network/pages/network.tsx | 178 +++++----- .../public/overview/pages/overview.test.tsx | 55 +-- .../public/overview/pages/overview.tsx | 171 +++++---- .../components/flyout/button/index.tsx | 25 +- .../timelines/components/timeline/index.tsx | 66 ++-- 25 files changed, 1215 insertions(+), 1358 deletions(-) diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx index 62b942d03591c..d033bc25e9801 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx @@ -12,9 +12,10 @@ import '../../../common/mock/match_media'; import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { DetectionEnginePageComponent } from './detection_engine'; import { useUserInfo } from '../../components/user_info'; +import { useWithSource } from '../../../common/containers/source'; jest.mock('../../components/user_info'); -jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/containers/source'); jest.mock('../../../common/components/link_to'); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -30,7 +31,12 @@ describe('DetectionEnginePageComponent', () => { beforeAll(() => { (useParams as jest.Mock).mockReturnValue({}); (useUserInfo as jest.Mock).mockReturnValue({}); + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); }); + it('renders correctly', () => { const wrapper = shallow( { /> ); - expect(wrapper.find('WithSource')).toHaveLength(1); + expect(wrapper.find('FiltersGlobal')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx index 05a0b4441bb3a..dc0b22c82af3e 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx @@ -13,10 +13,7 @@ import { useHistory } from 'react-router-dom'; import { SecurityPageName } from '../../../app/types'; import { TimelineId } from '../../../../common/types/timeline'; import { GlobalTime } from '../../../common/containers/global_time'; -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../../common/containers/source'; +import { useWithSource } from '../../../common/containers/source'; import { UpdateDateRange } from '../../../common/components/charts/common'; import { FiltersGlobal } from '../../../common/components/filters_global'; import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine'; @@ -82,6 +79,7 @@ export const DetectionEnginePageComponent: React.FC = ({ const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ signalIndexName, ]); + const { indicesExist, indexPattern } = useWithSource('default', indexToAdd); if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { return ( @@ -104,77 +102,73 @@ export const DetectionEnginePageComponent: React.FC = ({ <> {hasEncryptionKey != null && !hasEncryptionKey && } {hasIndexWrite != null && !hasIndexWrite && } - - {({ indicesExist, indexPattern }) => { - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - {i18n.LAST_ALERT} - {': '} - {lastAlerts} - - ) - } - title={i18n.PAGE_TITLE} - > - - {i18n.BUTTON_MANAGE_RULES} - - + {indicesExist ? ( + + + + + + + {i18n.LAST_ALERT} + {': '} + {lastAlerts} + + ) + } + title={i18n.PAGE_TITLE} + > + + {i18n.BUTTON_MANAGE_RULES} + + - - {({ to, from, deleteQuery, setQuery }) => ( - <> - <> - - - - - - )} - - - - ) : ( - - - - - ); - }} - + + {({ to, from, deleteQuery, setQuery }) => ( + <> + <> + + + + + + )} + + + + ) : ( + + + + + )} ); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx index df6ea65ba52ba..0acb18082379a 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx @@ -12,10 +12,12 @@ import { TestProviders } from '../../../../../common/mock'; import { RuleDetailsPageComponent } from './index'; import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; import { useUserInfo } from '../../../../components/user_info'; +import { useWithSource } from '../../../../../common/containers/source'; import { useParams } from 'react-router-dom'; jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); +jest.mock('../../../../../common/containers/source'); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -30,6 +32,10 @@ describe('RuleDetailsPageComponent', () => { beforeAll(() => { (useUserInfo as jest.Mock).mockReturnValue({}); (useParams as jest.Mock).mockReturnValue({}); + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); }); it('renders correctly', () => { @@ -44,6 +50,6 @@ describe('RuleDetailsPageComponent', () => { } ); - expect(wrapper.find('WithSource')).toHaveLength(1); + expect(wrapper.find('GlobalTime')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx index 90fd4bb225ec5..2ec603546983e 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable react-hooks/rules-of-hooks */ -/* eslint-disable complexity */ +/* eslint-disable react-hooks/rules-of-hooks, complexity */ // TODO: Disabling complexity is temporary till this component is refactored as part of lists UI integration import { @@ -36,10 +35,7 @@ import { SiemSearchBar } from '../../../../../common/components/search_bar'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; import { useRule } from '../../../../../alerts/containers/detection_engine/rules'; -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../../../../common/containers/source'; +import { useWithSource } from '../../../../../common/containers/source'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_about_rule_details'; @@ -255,6 +251,8 @@ export const RuleDetailsPageComponent: FC = ({ [history, ruleId] ); + const { indicesExist, indexPattern } = useWithSource('default', indexToAdd); + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { history.replace(getDetectionEngineUrl()); return null; @@ -264,187 +262,185 @@ export const RuleDetailsPageComponent: FC = ({ <> {hasIndexWrite != null && !hasIndexWrite && } {userHasNoPermissions(canUserCRUD) && } - - {({ indicesExist, indexPattern }) => { - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - {({ to, from, deleteQuery, setQuery }) => ( - - - - - - - - {detectionI18n.LAST_ALERT} - {': '} - {lastAlerts} - , - ] - : []), - , - ]} - title={title} - > - + {indicesExist ? ( + + {({ to, from, deleteQuery, setQuery }) => ( + + + + + + + + {detectionI18n.LAST_ALERT} + {': '} + {lastAlerts} + , + ] + : []), + , + ]} + title={title} + > + + + + + + + + + - - - + {ruleI18n.EDIT_RULE_SETTINGS} + - - - - - {ruleI18n.EDIT_RULE_SETTINGS} - - - - - - + - - {ruleError} - - - - - + + + + {ruleError} + + + + + - - - - - {defineRuleData != null && ( - - )} - - - - - - {scheduleRuleData != null && ( - - )} - - - + + + + + {defineRuleData != null && ( + + )} + + + + + + {scheduleRuleData != null && ( + + )} + + + + + {tabs} + + {ruleDetailTab === RuleDetailTabs.alerts && ( + <> + - {tabs} - - {ruleDetailTab === RuleDetailTabs.alerts && ( - <> - - - {ruleId != null && ( - - )} - - )} - {ruleDetailTab === RuleDetailTabs.exceptions && ( - )} - {ruleDetailTab === RuleDetailTabs.failures && } - - - )} - - ) : ( - - + + )} + {ruleDetailTab === RuleDetailTabs.exceptions && ( + + )} + {ruleDetailTab === RuleDetailTabs.failures && } + + + )} + + ) : ( + + - - - ); - }} - + + + )} ); }; +RuleDetailsPageComponent.displayName = 'RuleDetailsPageComponent'; + const makeMapStateToProps = () => { const getGlobalInputs = inputsSelectors.globalSelector(); return (state: State) => { @@ -467,3 +463,5 @@ const connector = connect(makeMapStateToProps, mapDispatchToProps); type PropsFromRedux = ConnectedProps; export const RuleDetailsPage = connector(memo(RuleDetailsPageComponent)); + +RuleDetailsPage.displayName = 'RuleDetailsPage'; diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index d8bdbd6e7ef5f..03e48282cb754 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -14,10 +14,7 @@ import { HeaderGlobal } from '../../common/components/header_global'; import { HelpMenu } from '../../common/components/help_menu'; import { AutoSaveWarningMsg } from '../../timelines/components/timeline/auto_save_warning'; import { UseUrlState } from '../../common/components/url_state'; -import { - WithSource, - indicesExistOrDataTemporarilyUnavailable, -} from '../../common/containers/source'; +import { useWithSource } from '../../common/containers/source'; import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { navTabs } from './home_navigations'; @@ -60,31 +57,28 @@ export const HomePage: React.FC = ({ children }) => { ); const [showTimeline] = useShowTimeline(); + const { browserFields, indexPattern, indicesExist } = useWithSource(); return (
- - {({ browserFields, indexPattern, indicesExist }) => ( - - - {indicesExistOrDataTemporarilyUnavailable(indicesExist) && showTimeline && ( - <> - - - - )} - - {children} - + + + {indicesExist && showTimeline && ( + <> + + + )} - + + {children} +
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index e60d876617dca..16207fcec3b26 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -6,10 +6,9 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { mocksSource } from '../../containers/source/mock'; -import { wait } from '../../lib/helpers'; +import { useWithSource } from '../../containers/source'; +import { mockBrowserFields } from '../../containers/source/mock'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; @@ -25,6 +24,14 @@ import { jest.mock('../link_to'); jest.mock('../../lib/kibana'); +jest.mock('../../containers/source', () => { + const original = jest.requireActual('../../containers/source'); + + return { + ...original, + useWithSource: jest.fn(), + }; +}); jest.mock('uuid', () => { return { @@ -52,6 +59,9 @@ describe('DraggableWrapperHoverContent', () => { beforeAll(() => { // our mock implementation of the useAddToTimeline hook returns a mock startDragToTimeline function: (useAddToTimeline as jest.Mock).mockReturnValue(jest.fn()); + (useWithSource as jest.Mock).mockReturnValue({ + browserFields: mockBrowserFields, + }); }); // Suppress warnings about "react-beautiful-dnd" @@ -323,17 +333,15 @@ describe('DraggableWrapperHoverContent', () => { test(`it ${assertion} the 'Add to timeline investigation' button when showTopN is ${showTopN}, value is ${maybeValue}, and a draggableId is ${maybeDraggableId}`, () => { const wrapper = mount( - - - + ); @@ -348,15 +356,13 @@ describe('DraggableWrapperHoverContent', () => { test('when clicked, it invokes the `startDragToTimeline` function returned by the `useAddToTimeline` hook', () => { const wrapper = mount( - - - + ); @@ -380,18 +386,15 @@ describe('DraggableWrapperHoverContent', () => { const aggregatableStringField = 'cloud.account.id'; const wrapper = mount( - - - + ); - await wait(); // https://github.com/apollographql/react-apollo/issues/1711 wrapper.update(); expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true); @@ -401,18 +404,15 @@ describe('DraggableWrapperHoverContent', () => { const whitelistedField = 'signal.rule.name'; const wrapper = mount( - - - + ); - await wait(); wrapper.update(); expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(true); @@ -422,18 +422,15 @@ describe('DraggableWrapperHoverContent', () => { const notKnownToBrowserFields = 'unknown.field'; const wrapper = mount( - - - + ); - await wait(); wrapper.update(); expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false); @@ -443,18 +440,15 @@ describe('DraggableWrapperHoverContent', () => { const whitelistedField = 'signal.rule.name'; const wrapper = mount( - - - + ); - await wait(); wrapper.update(); wrapper.find('[data-test-subj="show-top-field"]').first().simulate('click'); @@ -467,18 +461,15 @@ describe('DraggableWrapperHoverContent', () => { const whitelistedField = 'signal.rule.name'; const wrapper = mount( - - - + ); - await wait(); wrapper.update(); expect(wrapper.find('[data-test-subj="eventsByDatasetOverviewPanel"]').first().exists()).toBe( @@ -490,19 +481,16 @@ describe('DraggableWrapperHoverContent', () => { const whitelistedField = 'signal.rule.name'; const wrapper = mount( - - - + ); - await wait(); wrapper.update(); expect(wrapper.find('[data-test-subj="show-top-field"]').first().exists()).toBe(false); @@ -512,19 +500,16 @@ describe('DraggableWrapperHoverContent', () => { const whitelistedField = 'signal.rule.name'; const wrapper = mount( - - - + ); - await wait(); wrapper.update(); expect( diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index f916f42fe41cd..e805750cf2477 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -8,7 +8,7 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { DraggableId } from 'react-beautiful-dnd'; -import { getAllFieldsByName, WithSource } from '../../containers/source'; +import { getAllFieldsByName, useWithSource } from '../../containers/source'; import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { useKibana } from '../../lib/kibana'; @@ -79,6 +79,8 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [field, value, filterManager, onFilterAdded]); + const { browserFields } = useWithSource(); + return ( <> {!showTopN && value != null && ( @@ -117,40 +119,36 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ )} - - {({ browserFields }) => ( + <> + {allowTopN({ + browserField: getAllFieldsByName(browserFields)[field], + fieldName: field, + }) && ( <> - {allowTopN({ - browserField: getAllFieldsByName(browserFields)[field], - fieldName: field, - }) && ( - <> - {!showTopN && ( - - - - )} - - {showTopN && ( - - )} - + {!showTopN && ( + + + + )} + + {showTopN && ( + )} )} - + {!showTopN && ( diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index de19c1903586a..17fdf2163b58e 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -16,7 +16,7 @@ import { getAppOverviewUrl } from '../link_to'; import { MlPopover } from '../ml_popover/ml_popover'; import { SiemNavigation } from '../navigation'; import * as i18n from './translations'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; +import { useWithSource } from '../../containers/source'; import { useGetUrlSearch } from '../navigation/use_get_url_search'; import { useKibana } from '../../lib/kibana'; import { APP_ID, ADD_DATA_PATH, APP_ALERTS_PATH } from '../../../../common/constants'; @@ -41,6 +41,7 @@ interface HeaderGlobalProps { hideDetectionEngine?: boolean; } export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => { + const { indicesExist } = useWithSource(); const search = useGetUrlSearch(navTabs.overview); const { navigateToApp } = useKibana().services.application; const goToOverview = useCallback( @@ -54,60 +55,55 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine return ( - - {({ indicesExist }) => ( - <> - - - - - - - + <> + + + + + + + - - {indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - key !== SecurityPageName.alerts, navTabs) - : navTabs - } - /> - ) : ( - key === SecurityPageName.overview, navTabs)} - /> - )} - - + + {indicesExist ? ( + key !== SecurityPageName.alerts, navTabs) + : navTabs + } + /> + ) : ( + key === SecurityPageName.overview, navTabs)} + /> + )} + + - - - {indicesExistOrDataTemporarilyUnavailable(indicesExist) && - window.location.pathname.includes(APP_ALERTS_PATH) && ( - - - - )} + + + {indicesExist && window.location.pathname.includes(APP_ALERTS_PATH) && ( + + + + )} - - - {i18n.BUTTON_ADD_DATA} - - - + + + {i18n.BUTTON_ADD_DATA} + - - )} - + + + ); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx index c28f5ab8aa44f..09da027569c61 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx @@ -8,7 +8,7 @@ import React, { useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { GlobalTime } from '../../containers/global_time'; -import { BrowserFields, WithSource } from '../../containers/source'; +import { BrowserFields, useWithSource } from '../../containers/source'; import { useKibana } from '../../lib/kibana'; import { esQuery, Filter, Query } from '../../../../../../../src/plugins/data/public'; import { inputsModel, inputsSelectors, State } from '../../store'; @@ -99,7 +99,7 @@ const StatefulTopNComponent: React.FC = ({ // * `id` (`timelineId`) may only be populated when we are rendered in the // context of the active timeline. // * `indexToAdd`, which enables the alerts index to be appended to - // the `indexPattern` returned by `WithSource`, may only be populated when + // the `indexPattern` returned by `useWithSource`, may only be populated when // this component is rendered in the context of the active timeline. This // behavior enables the 'All events' view by appending the alerts index // to the index pattern. @@ -117,54 +117,50 @@ const StatefulTopNComponent: React.FC = ({ timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineEventType : undefined ); + const { indexPattern } = useWithSource('default', indexToAdd); + return ( {({ from, deleteQuery, setQuery, to }) => ( - - {({ indexPattern }) => ( - - )} - + )} ); diff --git a/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx index 9b9b5c5d815b9..9c9778c7074ee 100644 --- a/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/global_time/index.tsx @@ -94,3 +94,5 @@ export const connector = connect(mapStateToProps, mapDispatchToProps); type PropsFromRedux = ConnectedProps; export const GlobalTime = connector(React.memo(GlobalTimeComponent)); + +GlobalTime.displayName = 'GlobalTime'; diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx index d1a183a402e37..c30c3668638a3 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -4,55 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash/fp'; -import { mount } from 'enzyme'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; +import { act, renderHook } from '@testing-library/react-hooks'; -import { wait } from '../../lib/helpers'; - -import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '.'; +import { useWithSource, indicesExistOrDataTemporarilyUnavailable } from '.'; import { mockBrowserFields, mockIndexFields, mocksSource } from './mock'; jest.mock('../../lib/kibana'); +jest.mock('../../utils/apollo_context', () => ({ + useApolloClient: jest.fn().mockReturnValue({ + query: jest.fn().mockImplementation(() => Promise.resolve(mocksSource[0].result)), + }), +})); describe('Index Fields & Browser Fields', () => { - test('Index Fields', async () => { - mount( - - - {({ indexPattern }) => { - if (!isEqual(indexPattern.fields, [])) { - expect(indexPattern.fields).toEqual(mockIndexFields); - } + test('returns memoized value', async () => { + const { result, waitForNextUpdate, rerender } = renderHook(() => useWithSource()); + await waitForNextUpdate(); - return null; - }} - - - ); + const result1 = result.current; + act(() => rerender()); + const result2 = result.current; - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await wait(); + return expect(result1).toBe(result2); }); - test('Browser Fields', async () => { - mount( - - - {({ browserFields }) => { - if (!isEqual(browserFields, {})) { - expect(browserFields).toEqual(mockBrowserFields); - } + test('Index Fields', async () => { + const { result, waitForNextUpdate } = renderHook(() => useWithSource()); - return null; - }} - - - ); + await waitForNextUpdate(); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await wait(); + return expect(result).toEqual({ + current: { + indicesExist: true, + browserFields: mockBrowserFields, + indexPattern: { + fields: mockIndexFields, + title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*', + }, + loading: false, + errorMessage: null, + }, + error: undefined, + }); }); describe('indicesExistOrDataTemporarilyUnavailable', () => { diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index ad480ad2c496b..34ac5f8f5d94f 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -6,8 +6,7 @@ import { isUndefined } from 'lodash'; import { get, keyBy, pick, set, isEmpty } from 'lodash/fp'; -import { Query } from 'react-apollo'; -import React, { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import memoizeOne from 'memoize-one'; import { IIndexPattern } from 'src/plugins/data/public'; @@ -50,18 +49,6 @@ export const getAllFieldsByName = ( ): { [fieldName: string]: Partial } => keyBy('name', getAllBrowserFields(browserFields)); -interface WithSourceArgs { - indicesExist: boolean; - browserFields: BrowserFields; - indexPattern: IIndexPattern; -} - -interface WithSourceProps { - children: (args: WithSourceArgs) => React.ReactNode; - indexToAdd?: string[] | null; - sourceId: string; -} - export const getIndexFields = memoizeOne( (title: string, fields: IndexField[]): IIndexPattern => fields && fields.length > 0 @@ -71,7 +58,8 @@ export const getIndexFields = memoizeOne( ), title, } - : { fields: [], title } + : { fields: [], title }, + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length ); export const getBrowserFields = memoizeOne( @@ -82,10 +70,26 @@ export const getBrowserFields = memoizeOne( set([field.category, 'fields', field.name], field, accumulator), {} ) - : {} + : {}, + // Update the value only if _title has changed + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] ); -export const WithSource = React.memo(({ children, indexToAdd, sourceId }) => { +export const indicesExistOrDataTemporarilyUnavailable = ( + indicesExist: boolean | null | undefined +) => indicesExist || isUndefined(indicesExist); + +const EMPTY_BROWSER_FIELDS = {}; + +interface UseWithSourceState { + browserFields: BrowserFields; + errorMessage: string | null; + indexPattern: IIndexPattern; + indicesExist: boolean | undefined | null; + loading: boolean; +} + +export const useWithSource = (sourceId = 'default', indexToAdd?: string[] | null) => { const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const defaultIndex = useMemo(() => { if (indexToAdd != null && !isEmpty(indexToAdd)) { @@ -94,87 +98,84 @@ export const WithSource = React.memo(({ children, indexToAdd, s return configIndex; }, [configIndex, indexToAdd]); - return ( - - query={sourceQuery} - fetchPolicy="cache-first" - notifyOnNetworkStatusChange - variables={{ - sourceId, - defaultIndex, - }} - > - {({ data }) => - children({ - indicesExist: get('source.status.indicesExist', data), - browserFields: getBrowserFields( - defaultIndex.join(), - get('source.status.indexFields', data) - ), - indexPattern: getIndexFields(defaultIndex.join(), get('source.status.indexFields', data)), - }) - } - - ); -}); + const [state, setState] = useState({ + browserFields: EMPTY_BROWSER_FIELDS, + errorMessage: null, + indexPattern: getIndexFields(defaultIndex.join(), []), + indicesExist: undefined, + loading: false, + }); -WithSource.displayName = 'WithSource'; + const apolloClient = useApolloClient(); -export const indicesExistOrDataTemporarilyUnavailable = (indicesExist: boolean | undefined) => - indicesExist || isUndefined(indicesExist); + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); -export const useWithSource = (sourceId: string, indices: string[]) => { - const [loading, updateLoading] = useState(false); - const [indicesExist, setIndicesExist] = useState(undefined); - const [browserFields, setBrowserFields] = useState(null); - const [indexPattern, setIndexPattern] = useState(null); - const [errorMessage, updateErrorMessage] = useState(null); + async function fetchSource() { + if (!apolloClient) return; - const apolloClient = useApolloClient(); - async function fetchSource(signal: AbortSignal) { - updateLoading(true); - if (apolloClient) { - apolloClient - .query({ + setState((prevState) => ({ ...prevState, loading: true })); + + try { + const result = await apolloClient.query({ query: sourceQuery, fetchPolicy: 'cache-first', variables: { sourceId, - defaultIndex: indices, + defaultIndex, }, context: { fetchOptions: { - signal, + signal: abortCtrl.signal, }, }, - }) - .then( - (result) => { - updateLoading(false); - updateErrorMessage(null); - setIndicesExist(get('data.source.status.indicesExist', result)); - setBrowserFields( - getBrowserFields(indices.join(), get('data.source.status.indexFields', result)) - ); - setIndexPattern( - getIndexFields(indices.join(), get('data.source.status.indexFields', result)) - ); - }, - (error) => { - updateLoading(false); - updateErrorMessage(error.message); - } - ); + }); + if (!isSubscribed) { + return setState((prevState) => ({ + ...prevState, + loading: false, + })); + } + + setState({ + loading: false, + indicesExist: indicesExistOrDataTemporarilyUnavailable( + get('data.source.status.indicesExist', result) + ), + browserFields: getBrowserFields( + defaultIndex.join(), + get('data.source.status.indexFields', result) + ), + indexPattern: getIndexFields( + defaultIndex.join(), + get('data.source.status.indexFields', result) + ), + errorMessage: null, + }); + } catch (error) { + if (!isSubscribed) { + return setState((prevState) => ({ + ...prevState, + loading: false, + })); + } + + setState((prevState) => ({ + ...prevState, + loading: false, + errorMessage: error.message, + })); + } } - } - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchSource(signal); - return () => abortCtrl.abort(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [apolloClient, sourceId, indices]); + fetchSource(); + + return () => { + isSubscribed = false; + return abortCtrl.abort(); + }; + }, [apolloClient, sourceId, defaultIndex]); - return { indicesExist, browserFields, indexPattern, loading, errorMessage }; + return state; }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx index 936789625a4dd..e520facf285c2 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; import { MemoryRouter } from 'react-router-dom'; import useResizeObserver from 'use-resize-observer/polyfilled'; @@ -19,12 +18,7 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { getHostDetailsPageFilters } from './helpers'; jest.mock('../../../common/containers/source', () => ({ - indicesExistOrDataTemporarilyUnavailable: () => true, - WithSource: ({ - children, - }: { - children: (args: { indicesExist: boolean; indexPattern: IIndexPattern }) => React.ReactNode; - }) => children({ indicesExist: true, indexPattern: mockIndexPattern }), + useWithSource: jest.fn().mockReturnValue({ indicesExist: true, indexPattern: mockIndexPattern }), })); // Test will fail because we will to need to mock some core services to make the test work diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index e3f00a377d272..1c66a9edc1947 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -27,10 +27,7 @@ import { SiemSearchBar } from '../../../common/components/search_bar'; import { WrapperPage } from '../../../common/components/wrapper_page'; import { HostOverviewByNameQuery } from '../../containers/hosts/overview'; import { KpiHostDetailsQuery } from '../../containers/kpi_host_details'; -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../../common/containers/source'; +import { useWithSource } from '../../../common/containers/source'; import { LastEventIndexKey } from '../../../graphql/types'; import { useKibana } from '../../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../../common/lib/keury'; @@ -83,132 +80,126 @@ const HostDetailsComponent = React.memo( }, [setAbsoluteRangeDatePicker] ); + const { indicesExist, indexPattern } = useWithSource(); + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: getFilters(), + }); return ( <> - - {({ indicesExist, indexPattern }) => { - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: getFilters(), - }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - - } - title={detailName} - /> - - + + + + + + + } + title={detailName} + /> + + + {({ hostOverview, loading, id, inspect, refetch }) => ( + - {({ hostOverview, loading, id, inspect, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - - )} - - - - - - {({ kpiHostDetails, id, inspect, loading, refetch }) => ( - ( + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} /> )} - - - - - - - - - + )} + + + + + + {({ kpiHostDetails, id, inspect, loading, refetch }) => ( + - - - ) : ( - - - - - - ); - }} - + )} + + + + + + + + + + + + ) : ( + + + + + + )} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx index 85db3b4e159f1..ea0b32137eb39 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx @@ -5,15 +5,12 @@ */ import { mount } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; import React from 'react'; import { Router } from 'react-router-dom'; -import { MockedProvider } from 'react-apollo/test-utils'; import { Filter } from '../../../../../../src/plugins/data/common/es_query'; import '../../common/mock/match_media'; -import { mocksSource } from '../../common/containers/source/mock'; -import { wait } from '../../common/lib/helpers'; +import { useWithSource } from '../../common/containers/source'; import { apolloClientObservable, TestProviders, @@ -28,6 +25,8 @@ import { HostsComponentProps } from './types'; import { Hosts } from './hosts'; import { HostsTabs } from './hosts_tabs'; +jest.mock('../../common/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 and QueryBar jest.mock('../../common/components/search_bar', () => ({ @@ -37,19 +36,6 @@ jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); -let localSource: Array<{ - request: {}; - result: { - data: { - source: { - status: { - indicesExist: boolean; - }; - }; - }; - }; -}>; - type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; const location = { @@ -84,57 +70,49 @@ describe('Hosts - rendering', () => { hostsPagePath: '', }; - beforeEach(() => { - localSource = cloneDeep(mocksSource); - }); - test('it renders the Setup Instructions text when no index is available', async () => { - localSource[0].result.data.source.status.indicesExist = false; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: false, + }); + const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); }); test('it DOES NOT render the Setup Instructions text when an index is available', async () => { - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); test('it should render tab navigation', async () => { - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); + const wrapper = mount( - - - - - + + + ); - await wait(); - wrapper.update(); expect(wrapper.find(SiemNavigation).exists()).toBe(true); }); @@ -170,22 +148,21 @@ describe('Hosts - rendering', () => { }, }, ]; - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: { fields: [], title: 'title' }, + }); const myState: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const wrapper = mount( - - - - - + + + ); - await wait(); wrapper.update(); - myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters })); wrapper.update(); expect(wrapper.find(HostsTabs).props().filterQuery).toEqual( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index f6429544f855e..f5cc651a30443 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -22,10 +22,7 @@ import { manageQuery } from '../../common/components/page/manage_query'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { KpiHostsQuery } from '../containers/kpi_hosts'; -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../common/containers/source'; +import { useWithSource } from '../../common/containers/source'; import { LastEventIndexKey } from '../../graphql/types'; import { useKibana } from '../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../common/lib/keury'; @@ -77,87 +74,84 @@ export const HostsComponent = React.memo( }, [setAbsoluteRangeDatePicker] ); + const { indicesExist, indexPattern } = useWithSource(); + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }); + const tabsFilterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: tabsFilters, + }); return ( <> - - {({ indicesExist, indexPattern }) => { - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }); - const tabsFilterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: tabsFilters, - }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - } - title={i18n.PAGE_TITLE} - /> - - - {({ kpiHosts, loading, id, inspect, refetch }) => ( - - )} - - - - - - - - - + + + + + + } + title={i18n.PAGE_TITLE} + /> + + + {({ kpiHosts, loading, id, inspect, refetch }) => ( + - - - ) : ( - - - - - - ); - }} - + )} + + + + + + + + + + + + ) : ( + + + + + + )} diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap index 6e76ff00a8141..d7af8d6910f45 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap @@ -1,15 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Ip Details it matches the snapshot 1`] = ` - - - - +
+ + + + - +
`; diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx index bbb964ae17b9f..a87eb3d057447 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx @@ -5,15 +5,13 @@ */ import { shallow } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; import React from 'react'; import { Router } from 'react-router-dom'; -import { MockedProvider } from 'react-apollo/test-utils'; import { ActionCreator } from 'typescript-fsa'; import '../../../common/mock/match_media'; -import { mocksSource } from '../../../common/containers/source/mock'; +import { useWithSource } from '../../../common/containers/source'; import { FlowTarget } from '../../../graphql/types'; import { apolloClientObservable, @@ -32,6 +30,9 @@ const pop: Action = 'POP'; type GlobalWithFetch = NodeJS.Global & { fetch: jest.Mock }; +jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/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 and QueryBar jest.mock('../../../common/components/search_bar', () => ({ @@ -41,19 +42,6 @@ jest.mock('../../../common/components/query_bar', () => ({ QueryBar: () => null, })); -let localSource: Array<{ - request: {}; - result: { - data: { - source: { - status: { - indicesExist: boolean; - }; - }; - }; - }; -}>; - const getMockHistory = (ip: string) => ({ length: 2, location: { @@ -104,6 +92,10 @@ describe('Ip Details', () => { const mount = useMountAppended(); beforeAll(() => { + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: false, + indexPattern: {}, + }); (global as GlobalWithFetch).fetch = jest.fn().mockImplementationOnce(() => Promise.resolve({ ok: true, @@ -124,7 +116,6 @@ describe('Ip Details', () => { beforeEach(() => { store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); - localSource = cloneDeep(mocksSource); }); test('it renders', () => { @@ -138,20 +129,18 @@ describe('Ip Details', () => { }); test('it renders ipv6 headline', async () => { - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); const ip = 'fe80--24ce-f7ff-fede-a571'; const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); expect( wrapper .find('[data-test-subj="ip-details-headline"] [data-test-subj="header-page-title"]') diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx index face3f8904794..162b3a7c158d5 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx @@ -22,10 +22,7 @@ import { IpOverview } from '../../components/ip_overview'; import { SiemSearchBar } from '../../../common/components/search_bar'; import { WrapperPage } from '../../../common/components/wrapper_page'; import { IpOverviewQuery } from '../../containers/ip_overview'; -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../../common/containers/source'; +import { useWithSource } from '../../../common/containers/source'; import { FlowTargetSourceDest, LastEventIndexKey } from '../../../graphql/types'; import { useKibana } from '../../../common/lib/kibana'; import { decodeIpv6 } from '../../../common/lib/helpers'; @@ -74,208 +71,207 @@ export const IPDetailsComponent: React.FC { setIpDetailsTablesActivePageToZero(); }, [detailName, setIpDetailsTablesActivePageToZero]); - return ( - <> - - {({ indicesExist, indexPattern }) => { - const ip = decodeIpv6(detailName); - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }); + const { indicesExist, indexPattern } = useWithSource(); + const ip = decodeIpv6(detailName); + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(uiSettings), + indexPattern, + queries: [query], + filters, + }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - + return ( +
+ {indicesExist ? ( + + + + - - } - title={ip} - > - - + + } + title={ip} + > + + - + {({ id, inspect, ipOverviewData, loading, refetch }) => ( + - {({ id, inspect, ipOverviewData, loading, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - - )} - - )} - - - - - - - ( + - - - - - - - - - - - - - - - - - - + )} + + )} + - + - + + + - - - + + + - + - + + + - - - + - - - ) : ( - - + + + + + + + + + + + + + + + + + + + + + ) : ( + + - - - ); - }} - + + + )} - +
); }; IPDetailsComponent.displayName = 'IPDetailsComponent'; diff --git a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx index e1078dee3eb0d..7cdfdbf0af69a 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx @@ -5,14 +5,12 @@ */ import { mount } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; import React from 'react'; import { Router } from 'react-router-dom'; -import { MockedProvider } from 'react-apollo/test-utils'; import '../../common/mock/match_media'; import { Filter } from '../../../../../../src/plugins/data/common/es_query'; -import { mocksSource } from '../../common/containers/source/mock'; +import { useWithSource } from '../../common/containers/source'; import { TestProviders, mockGlobalState, @@ -26,6 +24,8 @@ import { inputsActions } from '../../common/store/inputs'; import { Network } from './network'; import { NetworkRoutes } from './navigation'; +jest.mock('../../common/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 and QueryBar jest.mock('../../common/components/search_bar', () => ({ @@ -35,19 +35,6 @@ jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); -let localSource: Array<{ - request: {}; - result: { - data: { - source: { - status: { - indicesExist: boolean; - }; - }; - }; - }; -}>; - type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; const location = { @@ -84,41 +71,33 @@ const getMockProps = () => ({ }); describe('rendering - rendering', () => { - beforeEach(() => { - localSource = cloneDeep(mocksSource); - }); - test('it renders the Setup Instructions text when no index is available', async () => { - localSource[0].result.data.source.status.indicesExist = false; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: false, + }); + const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); }); test('it DOES NOT render the Setup Instructions text when an index is available', async () => { - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); @@ -154,20 +133,20 @@ describe('rendering - rendering', () => { }, }, ]; - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: { fields: [], title: 'title' }, + }); const myState: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage); const wrapper = mount( - - - - - + + + ); - await new Promise((resolve) => setTimeout(resolve)); wrapper.update(); myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters })); diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index 845a6bbd95dd6..4275c1641f517 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -23,10 +23,7 @@ import { KpiNetworkComponent } from '..//components/kpi_network'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { KpiNetworkQuery } from '../../network/containers/kpi_network'; -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../common/containers/source'; +import { useWithSource } from '../../common/containers/source'; import { LastEventIndexKey } from '../../graphql/types'; import { useKibana } from '../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../common/lib/keury'; @@ -78,103 +75,100 @@ const NetworkComponent = React.memo( [setAbsoluteRangeDatePicker] ); + const { indicesExist, indexPattern } = useWithSource(sourceId); + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }); + const tabsFilterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: tabsFilters, + }); + return ( <> - - {({ indicesExist, indexPattern }) => { - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }); - const tabsFilterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: tabsFilters, - }); - - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - } - title={i18n.PAGE_TITLE} - /> - - + + + + + + } + title={i18n.PAGE_TITLE} + /> + + + + + + + {({ kpiNetwork, loading, id, inspect, refetch }) => ( + + )} + + {capabilitiesFetched && !isInitializing ? ( + <> - - {({ kpiNetwork, loading, id, inspect, refetch }) => ( - - )} - - - {capabilitiesFetched && !isInitializing ? ( - <> - - - - - - - - - ) : ( - - )} + - - - ) : ( - - - - - ); - }} - + + + + ) : ( + + )} + + + +
+ ) : ( + + + + + )} diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index a2010f1f64b71..d6e8fb984ac0f 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -5,17 +5,16 @@ */ import { mount } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; import { MemoryRouter } from 'react-router-dom'; import '../../common/mock/match_media'; import { TestProviders } from '../../common/mock'; -import { mocksSource } from '../../common/containers/source/mock'; +import { useWithSource } from '../../common/containers/source'; import { Overview } from './index'; jest.mock('../../common/lib/kibana'); +jest.mock('../../common/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 and QueryBar @@ -26,56 +25,36 @@ jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); -let localSource: Array<{ - request: {}; - result: { - data: { - source: { - status: { - indicesExist: boolean; - }; - }; - }; - }; -}>; - describe('Overview', () => { describe('rendering', () => { - beforeEach(() => { - localSource = cloneDeep(mocksSource); - }); - test('it renders the Setup Instructions text when no index is available', async () => { - localSource[0].result.data.source.status.indicesExist = false; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: false, + }); + const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); + expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); }); test('it DOES NOT render the Getting started text when an index is available', async () => { - localSource[0].result.data.source.status.indicesExist = true; + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); const wrapper = mount( - - - - - + + + ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise((resolve) => setTimeout(resolve)); - wrapper.update(); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 543dafd50c8e0..53cb32a16a9de 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -16,10 +16,7 @@ import { FiltersGlobal } from '../../common/components/filters_global'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { GlobalTime } from '../../common/containers/global_time'; -import { - WithSource, - indicesExistOrDataTemporarilyUnavailable, -} from '../../common/containers/source'; +import { useWithSource } from '../../common/containers/source'; import { EventsByDataset } from '../components/events_by_dataset'; import { EventCounts } from '../components/event_counts'; import { OverviewEmpty } from '../components/overview_empty'; @@ -41,89 +38,89 @@ const OverviewComponent: React.FC = ({ filters = NO_FILTERS, query = DEFAULT_QUERY, setAbsoluteRangeDatePicker, -}) => ( - <> - - {({ indicesExist, indexPattern }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - - - - - - - - {({ from, deleteQuery, setQuery, to }) => ( - - - - - - - - - - - - - - - - - - - )} - - - - - - ) : ( - - ) - } - - - - -); +}) => { + const { indicesExist, indexPattern } = useWithSource(); + + return ( + <> + {indicesExist ? ( + + + + + + + + + + + + + + {({ from, deleteQuery, setQuery, to }) => ( + + + + + + + + + + + + + + + + + + + )} + + + + + + ) : ( + + )} + + + + ); +}; const makeMapStateToProps = () => { const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx index ae05d99b58ee0..a1392ad8b8270 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/button/index.tsx @@ -10,7 +10,7 @@ import { rgba } from 'polished'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { WithSource } from '../../../../common/containers/source'; +import { useWithSource } from '../../../../common/containers/source'; import { IS_DRAGGING_CLASS_NAME } from '../../../../common/components/drag_and_drop/helpers'; import { DataProvider } from '../../timeline/data_providers/data_provider'; import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers'; @@ -84,6 +84,7 @@ interface FlyoutButtonProps { export const FlyoutButton = React.memo( ({ onOpen, show, dataProviders, timelineId }) => { const badgeCount = useMemo(() => getBadgeCount(dataProviders), [dataProviders]); + const { browserFields } = useWithSource(); if (!show) { return null; @@ -121,19 +122,15 @@ export const FlyoutButton = React.memo( - - {({ browserFields }) => ( - - )} - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 51cfe8ae33b05..df76eb350ace7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -9,7 +9,7 @@ import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { NO_ALERT_INDEX } from '../../../../common/constants'; -import { WithSource } from '../../../common/containers/source'; +import { useWithSource } from '../../../common/containers/source'; import { useSignalIndex } from '../../../alerts/containers/detection_engine/alerts/use_signal_index'; import { inputsModel, inputsSelectors, State } from '../../../common/store'; import { timelineActions, timelineSelectors } from '../../store/timeline'; @@ -158,40 +158,38 @@ const StatefulTimelineComponent = React.memo( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const { indexPattern, browserFields } = useWithSource('default', indexToAdd); + return ( - - {({ indexPattern, browserFields }) => ( - - )} - + ); }, (prevProps, nextProps) => { From aeae845e8455913e39d1d76a7d5eb2b4bd49b50b Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Thu, 25 Jun 2020 20:00:14 +0200 Subject: [PATCH 47/82] [QA] Unskip functional tests (#69760) (#69967) * [functional tests] unskip dashboard state * [functional tests] unskip empty dashboard, reference ES issue * [functional tests] unskip data_table_nontime_index * [functional tests] unskip viz builder tests * link existing issue Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../apps/dashboard/dashboard_state.js | 6 +- .../apps/dashboard/empty_dashboard.js | 3 +- test/functional/apps/discover/_errors.js | 2 +- .../visualize/_data_table_nontimeindex.js | 98 ++++++++----------- test/functional/apps/visualize/_tsvb_chart.ts | 7 +- 5 files changed, 49 insertions(+), 67 deletions(-) diff --git a/test/functional/apps/dashboard/dashboard_state.js b/test/functional/apps/dashboard/dashboard_state.js index 5bba2447cde28..3656c824394f4 100644 --- a/test/functional/apps/dashboard/dashboard_state.js +++ b/test/functional/apps/dashboard/dashboard_state.js @@ -251,8 +251,7 @@ export default function ({ getService, getPageObjects }) { }); }); - // Unskip once https://github.com/elastic/kibana/issues/15736 is fixed. - it.skip('and updates the pie slice legend color', async function () { + it('and updates the pie slice legend color', async function () { await retry.try(async () => { const colorExists = await PageObjects.visChart.doesSelectedLegendColorExist('#FFFFFF'); expect(colorExists).to.be(true); @@ -272,8 +271,7 @@ export default function ({ getService, getPageObjects }) { }); }); - // Unskip once https://github.com/elastic/kibana/issues/15736 is fixed. - it.skip('resets the legend color as well', async function () { + it('resets the legend color as well', async function () { await retry.try(async () => { const colorExists = await PageObjects.visChart.doesSelectedLegendColorExist('#57c17b'); expect(colorExists).to.be(true); diff --git a/test/functional/apps/dashboard/empty_dashboard.js b/test/functional/apps/dashboard/empty_dashboard.js index e7ebbcf09e828..7f13aca438842 100644 --- a/test/functional/apps/dashboard/empty_dashboard.js +++ b/test/functional/apps/dashboard/empty_dashboard.js @@ -49,10 +49,11 @@ export default function ({ getService, getPageObjects }) { expect(emptyWidgetExists).to.be(true); }); - it.skip('should open add panel when add button is clicked', async () => { + it('should open add panel when add button is clicked', async () => { await testSubjects.click('dashboardAddPanelButton'); const isAddPanelOpen = await dashboardAddPanel.isAddPanelOpen(); expect(isAddPanelOpen).to.be(true); + await testSubjects.click('euiFlyoutCloseButton'); }); it('should add new visualization from dashboard', async () => { diff --git a/test/functional/apps/discover/_errors.js b/test/functional/apps/discover/_errors.js index 5113fc8568d52..f3936d06bb6df 100644 --- a/test/functional/apps/discover/_errors.js +++ b/test/functional/apps/discover/_errors.js @@ -35,7 +35,7 @@ export default function ({ getService, getPageObjects }) { await esArchiver.unload('invalid_scripted_field'); }); - // https://github.com/elastic/kibana/issues/61366 + // ES issue https://github.com/elastic/elasticsearch/issues/54235 describe.skip('invalid scripted field error', () => { it('is rendered', async () => { const isFetchErrorVisible = await testSubjects.exists('discoverFetchError'); diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.js b/test/functional/apps/visualize/_data_table_nontimeindex.js index 4ae66d14ec30d..d64629a65c2c3 100644 --- a/test/functional/apps/visualize/_data_table_nontimeindex.js +++ b/test/functional/apps/visualize/_data_table_nontimeindex.js @@ -27,7 +27,7 @@ export default function ({ getService, getPageObjects }) { const renderable = getService('renderable'); const PageObjects = getPageObjects(['visualize', 'visEditor', 'header', 'visChart']); - describe.skip('data table with index without time filter', function indexPatternCreation() { + describe('data table with index without time filter', function indexPatternCreation() { const vizName1 = 'Visualization DataTable without time filter'; before(async function () { @@ -112,65 +112,49 @@ export default function ({ getService, getPageObjects }) { expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']); }); - it('should show correct data for a data table with date histogram', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch( - PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED - ); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.setInterval('Daily'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - log.debug(data.split('\n')); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-20', - '4,757', - '2015-09-21', - '4,614', - '2015-09-22', - '4,633', - ]); - }); + // bug https://github.com/elastic/kibana/issues/68977 + describe.skip('data table with date histogram', async () => { + before(async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickDataTable(); + await PageObjects.visualize.clickNewSearch( + PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED + ); + await PageObjects.visEditor.clickBucket('Split rows'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.selectField('@timestamp'); + await PageObjects.visEditor.setInterval('Daily'); + await PageObjects.visEditor.clickGo(); + }); - it('should show correct data for a data table with date histogram', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickDataTable(); - await PageObjects.visualize.clickNewSearch( - PageObjects.visualize.index.LOGSTASH_NON_TIME_BASED - ); - await PageObjects.visEditor.clickBucket('Split rows'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.selectField('@timestamp'); - await PageObjects.visEditor.setInterval('Daily'); - await PageObjects.visEditor.clickGo(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql([ - '2015-09-20', - '4,757', - '2015-09-21', - '4,614', - '2015-09-22', - '4,633', - ]); - }); + it('should show correct data', async () => { + const data = await PageObjects.visChart.getTableVisData(); + log.debug(data.split('\n')); + expect(data.trim().split('\n')).to.be.eql([ + '2015-09-20', + '4,757', + '2015-09-21', + '4,614', + '2015-09-22', + '4,633', + ]); + }); - it('should correctly filter for applied time filter on the main timefield', async () => { - await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); - }); + it('should correctly filter for applied time filter on the main timefield', async () => { + await filterBar.addFilter('@timestamp', 'is between', '2015-09-19', '2015-09-21'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await renderable.waitForRender(); + const data = await PageObjects.visChart.getTableVisData(); + expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); + }); - it('should correctly filter for pinned filters', async () => { - await filterBar.toggleFilterPinned('@timestamp'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await renderable.waitForRender(); - const data = await PageObjects.visChart.getTableVisData(); - expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); + it('should correctly filter for pinned filters', async () => { + await filterBar.toggleFilterPinned('@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await renderable.waitForRender(); + const data = await PageObjects.visChart.getTableVisData(); + expect(data.trim().split('\n')).to.be.eql(['2015-09-20', '4,757']); + }); }); }); } diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index f1c5c916a89bf..7e22f543bc7db 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -28,8 +28,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const security = getService('security'); const PageObjects = getPageObjects(['visualize', 'visualBuilder', 'timePicker', 'visChart']); - // FLAKY: https://github.com/elastic/kibana/issues/43150 - describe.skip('visual builder', function describeIndexTests() { + describe('visual builder', function describeIndexTests() { this.tags('includeFirefox'); beforeEach(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); @@ -74,7 +73,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/46677 describe('gauge', () => { beforeEach(async () => { await PageObjects.visualBuilder.resetPage(); @@ -107,7 +105,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('switch index patterns', () => { + // FLAKY: https://github.com/elastic/kibana/issues/43150 + describe.skip('switch index patterns', () => { beforeEach(async () => { log.debug('Load kibana_sample_data_flights data'); await esArchiver.loadIfNeeded('kibana_sample_data_flights'); From 4433deafc2e8579e8bbd8647ebf2d82982a996b7 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 25 Jun 2020 20:21:18 +0200 Subject: [PATCH 48/82] [7.x] [APM] Catch annotations index permission error (#69881) (#69947) Relates to #69642. If the user doesn't have the appropriate privileges for the annotations index, instead of failing with a 500, we now catch the error and log a warning to the console. --- .../services/annotations/get_stored_annotations.ts | 12 +++++++++++- .../apm/server/lib/services/annotations/index.ts | 5 ++++- x-pack/plugins/apm/server/routes/services.ts | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts index 2409da59d66ae..e77307a3f9db1 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { APICaller } from 'kibana/server'; +import { APICaller, Logger } from 'kibana/server'; import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ESSearchResponse } from '../../../../typings/elasticsearch'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; @@ -19,12 +19,14 @@ export async function getStoredAnnotations({ environment, apiCaller, annotationsClient, + logger, }: { setup: Setup & SetupTimeRange; serviceName: string; environment?: string; apiCaller: APICaller; annotationsClient: ScopedAnnotationsClient; + logger: Logger; }): Promise { try { const environmentFilter = getEnvironmentUiFilterES(environment); @@ -71,6 +73,14 @@ export async function getStoredAnnotations({ if (error.body?.error?.type === 'index_not_found_exception') { return []; } + + if (error.body?.error?.type === 'security_exception') { + logger.warn( + `Unable to get stored annotations due to a security exception. Please make sure that the user has 'indices:data/read/search' permissions for ${annotationsClient.index}` + ); + return []; + } + throw error; } } diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.ts index 9365213a87f6e..e2b6e74d4d65a 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.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 { APICaller } from 'kibana/server'; +import { APICaller, Logger } from 'kibana/server'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; import { getDerivedServiceAnnotations } from './get_derived_service_annotations'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -15,12 +15,14 @@ export async function getServiceAnnotations({ environment, annotationsClient, apiCaller, + logger, }: { serviceName: string; environment?: string; setup: Setup & SetupTimeRange; annotationsClient?: ScopedAnnotationsClient; apiCaller: APICaller; + logger: Logger; }) { // start fetching derived annotations (based on transactions), but don't wait on it // it will likely be significantly slower than the stored annotations @@ -37,6 +39,7 @@ export async function getServiceAnnotations({ environment, annotationsClient, apiCaller, + logger, }) : []; diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 8672c6c108c4c..08eba00251e26 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -105,6 +105,7 @@ export const serviceAnnotationsRoute = createRoute(() => ({ environment, annotationsClient, apiCaller: context.core.elasticsearch.legacy.client.callAsCurrentUser, + logger: context.logger, }); }, })); From 08ced7624602d699d56a98934885b37e89df1dbd Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Thu, 25 Jun 2020 14:23:27 -0400 Subject: [PATCH 49/82] Fixes special clicks and 3rd party icon sizes in nav (#69767) (#69950) --- .../chrome/ui/header/collapsible_nav.tsx | 22 ++++++++++--------- src/core/public/chrome/ui/header/nav_link.tsx | 22 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 9494e22920de8..3800e30c90cd6 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -38,7 +38,7 @@ import { AppCategory } from '../../../../types'; import { InternalApplicationStart } from '../../../application/types'; import { HttpStart } from '../../../http'; import { OnIsLockedUpdate } from './'; -import { createEuiListItem, createRecentNavLink } from './nav_link'; +import { createEuiListItem, createRecentNavLink, isModifiedOrPrevented } from './nav_link'; function getAllCategories(allCategorizedLinks: Record) { const allCategories = {} as Record; @@ -150,17 +150,13 @@ export function CollapsibleNav({ label: 'Home', iconType: 'home', href: homeHref, - onClick: (event: React.MouseEvent) => { - closeNav(); - if ( - event.isDefaultPrevented() || - event.altKey || - event.metaKey || - event.ctrlKey - ) { + onClick: (event) => { + if (isModifiedOrPrevented(event)) { return; } + event.preventDefault(); + closeNav(); navigateToApp('home'); }, }, @@ -196,7 +192,13 @@ export function CollapsibleNav({ return { ...hydratedLink, 'data-test-subj': 'collapsibleNavAppLink--recent', - onClick: closeNav, + onClick: (event) => { + if (isModifiedOrPrevented(event)) { + return; + } + + closeNav(); + }, }; })} maxWidth="none" diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index c09b15fac9bdb..de3a6da37587e 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -17,20 +17,15 @@ * under the License. */ -import { EuiImage } from '@elastic/eui'; +import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem, CoreStart } from '../../..'; import { HttpStart } from '../../../http'; import { relativeToAbsolute } from '../../nav_links/to_nav_link'; -function isModifiedEvent(event: React.MouseEvent) { - return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); -} - -function LinkIcon({ url }: { url: string }) { - return ; -} +export const isModifiedOrPrevented = (event: React.MouseEvent) => + event.metaKey || event.altKey || event.ctrlKey || event.shiftKey || event.defaultPrevented; interface Props { link: ChromeNavLink; @@ -62,13 +57,15 @@ export function createEuiListItem({ href, /* Use href and onClick to support "open in new tab" and SPA navigation in the same link */ onClick(event: React.MouseEvent) { - onClick(); + if (!isModifiedOrPrevented(event)) { + onClick(); + } + if ( !legacyMode && // ignore when in legacy mode !legacy && // ignore links to legacy apps - !event.defaultPrevented && // onClick prevented default event.button === 0 && // ignore everything but left clicks - !isModifiedEvent(event) // ignore clicks with modifier keys + !isModifiedOrPrevented(event) ) { event.preventDefault(); navigateToApp(id); @@ -80,7 +77,8 @@ export function createEuiListItem({ 'data-test-subj': dataTestSubj, ...(basePath && { iconType: euiIconType, - icon: !euiIconType && icon ? : undefined, + icon: + !euiIconType && icon ? : undefined, }), }; } From b833d6b419640a3d34d5eaae0a75567a18fe70c0 Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Thu, 25 Jun 2020 14:36:57 -0400 Subject: [PATCH 50/82] PR: Provide limit warnings to user when API limits are reached. (#69590) (#69944) * Provide facilties to raise limit warnings for user when API limits are reached. --- .../public/resolver/store/data/action.ts | 7 +- .../resolver/store/data/graphing.test.ts | 72 +++++++++++++++++-- .../public/resolver/store/data/reducer.ts | 6 +- .../public/resolver/store/data/selectors.ts | 12 ++++ .../public/resolver/store/middleware.ts | 9 ++- .../public/resolver/store/selectors.ts | 9 +++ .../public/resolver/types.ts | 3 +- .../public/resolver/view/use_camera.test.tsx | 7 +- 8 files changed, 111 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index fbeeefe1ab9f2..3de6f08f5e015 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -12,8 +12,11 @@ import { interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; - readonly events: ResolverEvent[]; - readonly stats: Map; + readonly payload: { + readonly events: Readonly; + readonly stats: Readonly>; + readonly lineageLimits: { readonly children: string | null; readonly ancestors: string | null }; + }; } interface ServerFailedToReturnResolverData { diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/graphing.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/graphing.test.ts index d120adb72cd81..163846e0414db 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/graphing.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/graphing.test.ts @@ -9,8 +9,13 @@ import { DataAction } from './action'; import { dataReducer } from './reducer'; import { DataState } from '../../types'; import { LegacyEndpointEvent, ResolverEvent } from '../../../../common/endpoint/types'; -import { graphableProcesses, processNodePositionsAndEdgeLineSegments } from './selectors'; +import { + graphableProcesses, + processNodePositionsAndEdgeLineSegments, + limitsReached, +} from './selectors'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; +import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; describe('resolver graph layout', () => { let processA: LegacyEndpointEvent; @@ -114,7 +119,10 @@ describe('resolver graph layout', () => { describe('when rendering no nodes', () => { beforeEach(() => { const events: ResolverEvent[] = []; - const action: DataAction = { type: 'serverReturnedResolverData', events, stats: new Map() }; + const action: DataAction = { + type: 'serverReturnedResolverData', + payload: { events, stats: new Map(), lineageLimits: { children: null, ancestors: null } }, + }; store.dispatch(action); }); it('the graphableProcesses list should only include nothing', () => { @@ -128,7 +136,10 @@ describe('resolver graph layout', () => { describe('when rendering one node', () => { beforeEach(() => { const events = [processA]; - const action: DataAction = { type: 'serverReturnedResolverData', events, stats: new Map() }; + const action: DataAction = { + type: 'serverReturnedResolverData', + payload: { events, stats: new Map(), lineageLimits: { children: null, ancestors: null } }, + }; store.dispatch(action); }); it('the graphableProcesses list should only include nothing', () => { @@ -142,7 +153,10 @@ describe('resolver graph layout', () => { describe('when rendering two nodes, one being the parent of the other', () => { beforeEach(() => { const events = [processA, processB]; - const action: DataAction = { type: 'serverReturnedResolverData', events, stats: new Map() }; + const action: DataAction = { + type: 'serverReturnedResolverData', + payload: { events, stats: new Map(), lineageLimits: { children: null, ancestors: null } }, + }; store.dispatch(action); }); it('the graphableProcesses list should only include nothing', () => { @@ -166,7 +180,10 @@ describe('resolver graph layout', () => { processH, processI, ]; - const action: DataAction = { type: 'serverReturnedResolverData', events, stats: new Map() }; + const action: DataAction = { + type: 'serverReturnedResolverData', + payload: { events, stats: new Map(), lineageLimits: { children: null, ancestors: null } }, + }; store.dispatch(action); }); it("the graphableProcesses list should only include events with 'processCreated' an 'processRan' eventType", () => { @@ -187,3 +204,48 @@ describe('resolver graph layout', () => { }); }); }); + +describe('resolver graph with too much lineage', () => { + let generator: EndpointDocGenerator; + let store: Store; + let allEvents: ResolverEvent[]; + let childrenCursor: string; + let ancestorCursor: string; + + beforeEach(() => { + generator = new EndpointDocGenerator('seed'); + allEvents = generator.generateTree({ ancestors: 1, generations: 2, children: 2 }).allEvents; + childrenCursor = 'aValidChildursor'; + ancestorCursor = 'aValidAncestorCursor'; + store = createStore(dataReducer, undefined); + }); + + describe('should select from state properly', () => { + it('should indicate there are too many ancestors', () => { + const action: DataAction = { + type: 'serverReturnedResolverData', + payload: { + events: allEvents, + stats: new Map(), + lineageLimits: { children: childrenCursor, ancestors: ancestorCursor }, + }, + }; + store.dispatch(action); + const { ancestors } = limitsReached(store.getState()); + expect(ancestors).toEqual(true); + }); + it('should indicate there are too many children', () => { + const action: DataAction = { + type: 'serverReturnedResolverData', + payload: { + events: allEvents, + stats: new Map(), + lineageLimits: { children: childrenCursor, ancestors: ancestorCursor }, + }, + }; + store.dispatch(action); + const { children } = limitsReached(store.getState()); + expect(children).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index 3e897a91a74c6..a36d43b70b87d 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -13,6 +13,7 @@ function initialState(): DataState { relatedEventsStats: new Map(), relatedEvents: new Map(), relatedEventsReady: new Map(), + lineageLimits: { children: null, ancestors: null }, isLoading: false, hasError: false, }; @@ -22,8 +23,9 @@ export const dataReducer: Reducer = (state = initialS if (action.type === 'serverReturnedResolverData') { return { ...state, - results: action.events, - relatedEventsStats: action.stats, + results: action.payload.events, + relatedEventsStats: action.payload.stats, + lineageLimits: action.payload.lineageLimits, isLoading: false, hasError: false, }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index 2873993cc645f..ba415e6d83c8d 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -529,3 +529,15 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( }; } ); + +/** + * Returns the `children` and `ancestors` limits for the current graph, if any. + * + * @param state {DataState} the DataState from the reducer + */ +export const limitsReached = (state: DataState): { children: boolean; ancestors: boolean } => { + return { + children: state.lineageLimits.children !== null, + ancestors: state.lineageLimits.ancestors !== null, + }; +}; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts index 7f6f58dac7158..a352a076e5a97 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts @@ -77,6 +77,8 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => { } const nodeStats: Map = new Map(); nodeStats.set(entityId, stats); + const lineageLimits = { children: children.nextChild, ancestors: ancestry.nextAncestor }; + const events = [ ...lifecycle, ...getLifecycleEventsAndStats(children.childNodes, nodeStats), @@ -84,8 +86,11 @@ export const resolverMiddlewareFactory: MiddlewareFactory = (context) => { ]; api.dispatch({ type: 'serverReturnedResolverData', - events, - stats: nodeStats, + payload: { + events, + stats: nodeStats, + lineageLimits, + }, }); } catch (error) { api.dispatch({ diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index bff30c62864f2..3a5c48009e5bb 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -152,6 +152,15 @@ export const graphableProcesses = composeSelectors( dataSelectors.graphableProcesses ); +/** + * Select the `ancestors` and `children` limits that were reached or exceeded + * during the request for the current tree. + */ +export const lineageLimitsReached = composeSelectors( + dataStateSelector, + dataSelectors.limitsReached +); + /** * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a * concern-specific selector. `selector` should return the concern-specific state. diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index a48f3b59b0f6d..f0e401dd2e893 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -147,9 +147,10 @@ export type CameraState = { */ export interface DataState { readonly results: readonly ResolverEvent[]; - readonly relatedEventsStats: Map; + readonly relatedEventsStats: Readonly>; readonly relatedEvents: Map; readonly relatedEventsReady: Map; + readonly lineageLimits: Readonly<{ children: string | null; ancestors: string | null }>; isLoading: boolean; hasError: boolean; } diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index 8ed9f00d51af8..dc7cb9a2ab199 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -176,8 +176,11 @@ describe('useCamera on an unpainted element', () => { } const serverResponseAction: ResolverAction = { type: 'serverReturnedResolverData', - events, - stats: new Map(), + payload: { + events, + stats: new Map(), + lineageLimits: { children: null, ancestors: null }, + }, }; act(() => { store.dispatch(serverResponseAction); From 1deeddb2742ad3eec12a4eaf8ed4c1300aa9085c Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 25 Jun 2020 14:42:35 -0400 Subject: [PATCH 51/82] [Endpoint][Ingest Manager] minor code cleanup (#69844) (#69946) * Ingest: Rename datasource Layout prop to `onCancel` * Endpoint: Policy list - swap use of endpoint package hook for redux middleware * Endpoint: Add tests cases for `sendGetEndpointSecurityPackage()` method * Endpoint: add policy list store tests for new action --- .../components/layout.tsx | 6 +- .../create_datasource_page/index.tsx | 2 +- .../pages/policy/store/policy_list/action.ts | 13 ++- .../policy/store/policy_list/index.test.ts | 17 ++++ .../policy/store/policy_list/middleware.ts | 22 ++++- .../pages/policy/store/policy_list/reducer.ts | 8 ++ .../policy/store/policy_list/selectors.ts | 14 +++ .../store/policy_list/services/ingest.test.ts | 93 ++++++++++++++++++- .../store/policy_list/services/ingest.ts | 2 +- .../store/policy_list/test_mock_utils.ts | 79 +++++++++++++++- .../public/management/pages/policy/types.ts | 3 + .../pages/policy/view/ingest_hooks.ts | 44 --------- .../pages/policy/view/policy_list.tsx | 11 +-- 13 files changed, 254 insertions(+), 60 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_hooks.ts diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx index 7939feed80143..6f23c0ce60850 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx @@ -23,14 +23,14 @@ import { CreateDatasourceFrom } from '../types'; export const CreateDatasourcePageLayout: React.FunctionComponent<{ from: CreateDatasourceFrom; cancelUrl: string; - cancelOnClick?: React.ReactEventHandler; + onCancel?: React.ReactEventHandler; agentConfig?: AgentConfig; packageInfo?: PackageInfo; 'data-test-subj'?: string; }> = ({ from, cancelUrl, - cancelOnClick, + onCancel, agentConfig, packageInfo, children, @@ -45,7 +45,7 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ iconType="arrowLeft" flush="left" href={cancelUrl} - onClick={cancelOnClick} + onClick={onCancel} data-test-subj={`${dataTestSubj}_cancelBackLink`} > { const layoutProps = { from, cancelUrl, - cancelOnClick: cancelClickHandler, + onCancel: cancelClickHandler, agentConfig, packageInfo, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/action.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/action.ts index e14e39bf45c93..b04b2f085689e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/action.ts @@ -6,7 +6,10 @@ import { PolicyData } from '../../../../../../common/endpoint/types'; import { ServerApiError } from '../../../../../common/types'; -import { GetAgentStatusResponse } from '../../../../../../../ingest_manager/common/types/rest_spec'; +import { + GetAgentStatusResponse, + GetPackagesResponse, +} from '../../../../../../../ingest_manager/common'; interface ServerReturnedPolicyListData { type: 'serverReturnedPolicyListData'; @@ -53,6 +56,11 @@ interface ServerReturnedPolicyAgentsSummaryForDelete { payload: { agentStatusSummary: GetAgentStatusResponse['results'] }; } +interface ServerReturnedEndpointPackageInfo { + type: 'serverReturnedEndpointPackageInfo'; + payload: GetPackagesResponse['response'][0]; +} + export type PolicyListAction = | ServerReturnedPolicyListData | ServerFailedToReturnPolicyListData @@ -61,4 +69,5 @@ export type PolicyListAction = | ServerDeletedPolicy | UserOpenedPolicyListDeleteModal | ServerReturnedPolicyAgentsSummaryForDeleteFailure - | ServerReturnedPolicyAgentsSummaryForDelete; + | ServerReturnedPolicyAgentsSummaryForDelete + | ServerReturnedEndpointPackageInfo; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts index c24c47becc0b5..f454061055e96 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts @@ -18,6 +18,7 @@ import { selectIsLoading, urlSearchParams, selectIsDeleting, + endpointPackageVersion, } from './selectors'; import { DepsStartMock, depsStartMock } from '../../../../../common/mock/endpoint'; import { setPolicyListApiMockImplementation } from './test_mock_utils'; @@ -254,5 +255,21 @@ describe('policy list store concerns', () => { page_size: 50, }); }); + + it('should load package information only if not already in state', async () => { + dispatchUserChangedUrl('?page_size=10&page_index=10'); + await waitForAction('serverReturnedEndpointPackageInfo'); + expect(endpointPackageVersion(store.getState())).toEqual('0.5.0'); + fakeCoreStart.http.get.mockClear(); + dispatchUserChangedUrl('?page_size=10&page_index=11'); + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + query: { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + page: 12, + perPage: 10, + }, + }); + expect(endpointPackageVersion(store.getState())).toEqual('0.5.0'); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts index 39c685da3ec46..7d8620a5831d0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts @@ -9,8 +9,9 @@ import { sendGetEndpointSpecificDatasources, sendDeleteDatasource, sendGetFleetAgentStatusForConfig, + sendGetEndpointSecurityPackage, } from './services/ingest'; -import { isOnPolicyListPage, urlSearchParams } from './selectors'; +import { endpointPackageInfo, isOnPolicyListPage, urlSearchParams } from './selectors'; import { ImmutableMiddlewareFactory } from '../../../../../common/store'; import { initialPolicyListState } from './reducer'; import { @@ -32,6 +33,25 @@ export const policyListMiddlewareFactory: ImmutableMiddlewareFactory { + dispatch({ + type: 'serverReturnedEndpointPackageInfo', + payload: packageInfo, + }); + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error(error); + }); + } + const { page_index: pageIndex, page_size: pageSize } = urlSearchParams(state); let response: GetPolicyListResponse; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts index a8a2ad3e7cc26..52bed8d850ef4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/reducer.ts @@ -16,6 +16,7 @@ import { PolicyListState } from '../../types'; */ export const initialPolicyListState: () => Immutable = () => ({ policyItems: [], + endpointPackageInfo: undefined, isLoading: false, isDeleting: false, deleteStatus: undefined, @@ -95,6 +96,13 @@ export const policyListReducer: ImmutableReducer = ( }; } + if (action.type === 'serverReturnedEndpointPackageInfo') { + return { + ...state, + endpointPackageInfo: action.payload, + }; + } + if (action.type === 'userChangedUrl') { const newState: Immutable = { ...state, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/selectors.ts index 089c97b5520a2..ce57d238d7581 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/selectors.ts @@ -84,3 +84,17 @@ export const urlSearchParams: ( return searchParams; }); + +/** + * Returns package information for Endpoint + * @param state + */ +export const endpointPackageInfo = (state: Immutable) => state.endpointPackageInfo; + +/** + * Returns the version number for the endpoint package. + */ +export const endpointPackageVersion = createSelector( + endpointPackageInfo, + (info) => info?.version ?? undefined +); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts index cbbc5c3c6fdbe..2270c65fb149f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sendGetDatasource, sendGetEndpointSpecificDatasources } from './ingest'; +import { + sendGetDatasource, + sendGetEndpointSecurityPackage, + sendGetEndpointSpecificDatasources, +} from './ingest'; import { httpServiceMock } from '../../../../../../../../../../src/core/public/mocks'; import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../../ingest_manager/common'; @@ -37,6 +41,7 @@ describe('ingest service', () => { }); }); }); + describe('sendGetDatasource()', () => { it('builds correct API path', async () => { await sendGetDatasource(http, '123'); @@ -51,4 +56,90 @@ describe('ingest service', () => { }); }); }); + + describe('sendGetEndpointSecurityPackage()', () => { + it('should query EPM with category=security', async () => { + http.get.mockResolvedValue({ + response: [ + { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + description: 'This is the Elastic Endpoint package.', + type: 'solution', + download: '/epr/endpoint/endpoint-0.5.0.tar.gz', + path: '/package/endpoint/0.5.0', + icons: [ + { + src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', + size: '16x16', + type: 'image/svg+xml', + }, + ], + status: 'installed', + savedObject: { + type: 'epm-packages', + id: 'endpoint', + attributes: { + installed: [ + { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, + { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, + { id: 'logs-endpoint.alerts', type: 'index-template' }, + { id: 'events-endpoint', type: 'index-template' }, + { id: 'logs-endpoint.events.file', type: 'index-template' }, + { id: 'logs-endpoint.events.library', type: 'index-template' }, + { id: 'metrics-endpoint.metadata', type: 'index-template' }, + { id: 'metrics-endpoint.metadata_mirror', type: 'index-template' }, + { id: 'logs-endpoint.events.network', type: 'index-template' }, + { id: 'metrics-endpoint.policy', type: 'index-template' }, + { id: 'logs-endpoint.events.process', type: 'index-template' }, + { id: 'logs-endpoint.events.registry', type: 'index-template' }, + { id: 'logs-endpoint.events.security', type: 'index-template' }, + { id: 'metrics-endpoint.telemetry', type: 'index-template' }, + ], + es_index_patterns: { + alerts: 'logs-endpoint.alerts-*', + events: 'events-endpoint-*', + file: 'logs-endpoint.events.file-*', + library: 'logs-endpoint.events.library-*', + metadata: 'metrics-endpoint.metadata-*', + metadata_mirror: 'metrics-endpoint.metadata_mirror-*', + network: 'logs-endpoint.events.network-*', + policy: 'metrics-endpoint.policy-*', + process: 'logs-endpoint.events.process-*', + registry: 'logs-endpoint.events.registry-*', + security: 'logs-endpoint.events.security-*', + telemetry: 'metrics-endpoint.telemetry-*', + }, + name: 'endpoint', + version: '0.5.0', + internal: false, + removable: false, + }, + references: [], + updated_at: '2020-06-24T14:41:23.098Z', + version: 'Wzc0LDFd', + score: 0, + }, + }, + ], + success: true, + }); + await sendGetEndpointSecurityPackage(http); + expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/epm/packages', { + query: { category: 'security' }, + }); + }); + + it('should throw if package is not found', async () => { + http.get.mockResolvedValue({ response: [], success: true }); + await expect(async () => { + await sendGetEndpointSecurityPackage(http); + }).rejects.toThrow(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts index 66e98aa51601e..cbdd67261739f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -20,7 +20,7 @@ const INGEST_API_ROOT = `/api/ingest_manager`; export const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`; const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`; const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; -const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; +export const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; const INGEST_API_DELETE_DATASOURCE = `${INGEST_API_DATASOURCES}/delete`; /** diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts index 2c495202dc75b..0f0d1cb1b559d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -5,9 +5,14 @@ */ import { HttpStart } from 'kibana/public'; -import { INGEST_API_DATASOURCES } from './services/ingest'; +import { INGEST_API_DATASOURCES, INGEST_API_EPM_PACKAGES } from './services/ingest'; import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { GetPolicyListResponse } from '../../types'; +import { + AssetReference, + GetPackagesResponse, + InstallationStatus, +} from '../../../../../../../ingest_manager/common'; const generator = new EndpointDocGenerator('policy-list'); @@ -32,6 +37,78 @@ export const setPolicyListApiMockImplementation = ( success: true, }); } + + if (path === INGEST_API_EPM_PACKAGES) { + return Promise.resolve({ + response: [ + { + name: 'endpoint', + title: 'Elastic Endpoint', + version: '0.5.0', + description: 'This is the Elastic Endpoint package.', + type: 'solution', + download: '/epr/endpoint/endpoint-0.5.0.tar.gz', + path: '/package/endpoint/0.5.0', + icons: [ + { + src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', + size: '16x16', + type: 'image/svg+xml', + }, + ], + status: 'installed' as InstallationStatus, + savedObject: { + type: 'epm-packages', + id: 'endpoint', + attributes: { + installed: [ + { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' }, + { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' }, + { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' }, + { id: 'logs-endpoint.alerts', type: 'index-template' }, + { id: 'events-endpoint', type: 'index-template' }, + { id: 'logs-endpoint.events.file', type: 'index-template' }, + { id: 'logs-endpoint.events.library', type: 'index-template' }, + { id: 'metrics-endpoint.metadata', type: 'index-template' }, + { id: 'metrics-endpoint.metadata_mirror', type: 'index-template' }, + { id: 'logs-endpoint.events.network', type: 'index-template' }, + { id: 'metrics-endpoint.policy', type: 'index-template' }, + { id: 'logs-endpoint.events.process', type: 'index-template' }, + { id: 'logs-endpoint.events.registry', type: 'index-template' }, + { id: 'logs-endpoint.events.security', type: 'index-template' }, + { id: 'metrics-endpoint.telemetry', type: 'index-template' }, + ] as AssetReference[], + es_index_patterns: { + alerts: 'logs-endpoint.alerts-*', + events: 'events-endpoint-*', + file: 'logs-endpoint.events.file-*', + library: 'logs-endpoint.events.library-*', + metadata: 'metrics-endpoint.metadata-*', + metadata_mirror: 'metrics-endpoint.metadata_mirror-*', + network: 'logs-endpoint.events.network-*', + policy: 'metrics-endpoint.policy-*', + process: 'logs-endpoint.events.process-*', + registry: 'logs-endpoint.events.registry-*', + security: 'logs-endpoint.events.security-*', + telemetry: 'metrics-endpoint.telemetry-*', + }, + name: 'endpoint', + version: '0.5.0', + internal: false, + removable: false, + }, + references: [], + updated_at: '2020-06-24T14:41:23.098Z', + version: 'Wzc0LDFd', + }, + }, + ], + success: true, + }); + } } return Promise.reject(new Error(`MOCK: unknown policy list api: ${path}`)); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index 4d798d3717ce4..a3a0983331ac3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -16,6 +16,7 @@ import { GetAgentStatusResponse, GetDatasourcesResponse, GetOneDatasourceResponse, + GetPackagesResponse, UpdateDatasourceResponse, } from '../../../../../ingest_manager/common'; @@ -25,6 +26,8 @@ import { export interface PolicyListState { /** Array of policy items */ policyItems: PolicyData[]; + /** Information about the latest endpoint package */ + endpointPackageInfo?: GetPackagesResponse['response'][0]; /** API error if loading data failed */ apiError?: ServerApiError; /** total number of policies */ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_hooks.ts deleted file mode 100644 index 75e1556ff0bb0..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_hooks.ts +++ /dev/null @@ -1,44 +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 { useEffect, useState } from 'react'; -import { Immutable } from '../../../../../common/endpoint/types'; -import { GetPackagesResponse } from '../../../../../../ingest_manager/common/types/rest_spec'; -import { sendGetEndpointSecurityPackage } from '../store/policy_list/services/ingest'; -import { useKibana } from '../../../../common/lib/kibana'; - -type UseEndpointPackageInfo = [ - /** The Package Info. will be undefined while it is being fetched */ - Immutable | undefined, - /** Boolean indicating if fetching is underway */ - boolean, - /** Any error encountered during fetch */ - Error | undefined -]; - -/** - * Hook that fetches the endpoint package info - * - * @example - * const [packageInfo, isFetching, fetchError] = useEndpointPackageInfo(); - */ -export const useEndpointPackageInfo = (): UseEndpointPackageInfo => { - const { - services: { http }, - } = useKibana(); - const [endpointPackage, setEndpointPackage] = useState(); - const [isFetching, setIsFetching] = useState(true); - const [error, setError] = useState(); - - useEffect(() => { - sendGetEndpointSecurityPackage(http) - .then((packageInfo) => setEndpointPackage(packageInfo)) - .catch((apiError) => setError(apiError)) - .finally(() => setIsFetching(false)); - }, [http]); - - return [endpointPackage, isFetching, error]; -}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 4532408332d6e..26b6ecb540cd9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -48,7 +48,6 @@ import { useFormatUrl } from '../../../../common/components/link_to'; import { getPolicyDetailPath, getPoliciesPath } from '../../../common/routing'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { CreateDatasourceRouteState } from '../../../../../../ingest_manager/public'; -import { useEndpointPackageInfo } from './ingest_hooks'; interface TableChangeCallbackArguments { page: { index: number; size: number }; @@ -135,7 +134,6 @@ export const PolicyList = React.memo(() => { const [policyIdToDelete, setPolicyIdToDelete] = useState(''); const dispatch = useDispatch<(action: PolicyListAction) => void>(); - const [packageInfo, isFetchingPackageInfo] = useEndpointPackageInfo(); const { selectPolicyItems: policyItems, selectPageIndex: pageIndex, @@ -146,6 +144,7 @@ export const PolicyList = React.memo(() => { selectIsDeleting: isDeleting, selectDeleteStatus: deleteStatus, selectAgentStatusSummary: agentStatusSummary, + endpointPackageVersion, } = usePolicyListSelector(selector); const handleCreatePolicyClick = useNavigateToAppEventHandler( @@ -156,7 +155,9 @@ export const PolicyList = React.memo(() => { // Also, // We pass along soem state information so that the Ingest page can change the behaviour // of the cancel and submit buttons and redirect the user back to endpoint policy - path: `#/integrations${packageInfo ? `/endpoint-${packageInfo.version}/add-datasource` : ''}`, + path: `#/integrations${ + endpointPackageVersion ? `/endpoint-${endpointPackageVersion}/add-datasource` : '' + }`, state: { onCancelNavigateTo: ['securitySolution:management', { path: getPoliciesPath() }], onCancelUrl: formatUrl(getPoliciesPath()), @@ -401,7 +402,6 @@ export const PolicyList = React.memo(() => { { )} @@ -449,7 +449,6 @@ export const PolicyList = React.memo(() => { }, [ policyItems, loading, - isFetchingPackageInfo, columns, handleCreatePolicyClick, handleTableChange, From b87674a218534597606f865b34eade234bc66043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Thu, 25 Jun 2020 20:51:38 +0200 Subject: [PATCH 52/82] [7.x] [Logs UI] Logs ui context menu (#69915) (#69940) --- .../log_entry_actions_column.tsx | 120 ------------------ .../log_entry_context_menu.tsx | 94 ++++++++++++++ .../logging/log_text_stream/log_entry_row.tsx | 60 +++++++-- 3 files changed, 143 insertions(+), 131 deletions(-) delete mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx deleted file mode 100644 index e27de7fd6b5a8..0000000000000 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback } from 'react'; -import { EuiButtonIcon, EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { LogEntryColumnContent } from './log_entry_column'; -import { euiStyled } from '../../../../../observability/public'; - -interface LogEntryActionsColumnProps { - isHovered: boolean; - isMenuOpen: boolean; - onOpenMenu: () => void; - onCloseMenu: () => void; - onViewDetails?: () => void; - onViewLogInContext?: () => void; -} - -const MENU_LABEL = i18n.translate('xpack.infra.logEntryItemView.logEntryActionsMenuToolTip', { - defaultMessage: 'View actions for line', -}); - -const LOG_DETAILS_LABEL = i18n.translate('xpack.infra.logs.logEntryActionsDetailsButton', { - defaultMessage: 'View details', -}); - -const LOG_VIEW_IN_CONTEXT_LABEL = i18n.translate( - 'xpack.infra.lobs.logEntryActionsViewInContextButton', - { - defaultMessage: 'View in context', - } -); - -export const LogEntryActionsColumn: React.FC = ({ - isHovered, - isMenuOpen, - onOpenMenu, - onCloseMenu, - onViewDetails, - onViewLogInContext, -}) => { - const handleClickViewDetails = useCallback(() => { - onCloseMenu(); - - // Function might be `undefined` and the linter doesn't like that. - // eslint-disable-next-line no-unused-expressions - onViewDetails?.(); - }, [onCloseMenu, onViewDetails]); - - const handleClickViewInContext = useCallback(() => { - onCloseMenu(); - - // Function might be `undefined` and the linter doesn't like that. - // eslint-disable-next-line no-unused-expressions - onViewLogInContext?.(); - }, [onCloseMenu, onViewLogInContext]); - - const button = ( - - - - ); - - const items = [ - - {LOG_DETAILS_LABEL} - , - ]; - - if (onViewLogInContext !== undefined) { - items.push( - - {LOG_VIEW_IN_CONTEXT_LABEL} - - ); - } - - return ( - - {isHovered || isMenuOpen ? ( - - - - - - ) : null} - - ); -}; - -const ActionsColumnContent = euiStyled(LogEntryColumnContent)` - overflow: hidden; - user-select: none; -`; - -const ButtonWrapper = euiStyled.div` - background: ${(props) => props.theme.eui.euiColorPrimary}; - border-radius: 50%; - padding: 4px; - transform: translateY(-6px); -`; - -// this prevents the button from influencing the line height -const AbsoluteWrapper = euiStyled.div` - position: absolute; -`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx new file mode 100644 index 0000000000000..4aa81846d90ef --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; + +import { euiStyled } from '../../../../../observability/public'; +import { LogEntryColumnContent } from './log_entry_column'; + +interface LogEntryContextMenuItem { + label: string; + onClick: () => void; +} + +interface LogEntryContextMenuProps { + 'aria-label'?: string; + isOpen: boolean; + onOpen: () => void; + onClose: () => void; + items: LogEntryContextMenuItem[]; +} + +const DEFAULT_MENU_LABEL = i18n.translate( + 'xpack.infra.logEntryItemView.logEntryActionsMenuToolTip', + { + defaultMessage: 'View actions for line', + } +); + +export const LogEntryContextMenu: React.FC = ({ + 'aria-label': ariaLabel, + isOpen, + onOpen, + onClose, + items, +}) => { + const closeMenuAndCall = useMemo(() => { + return (callback: LogEntryContextMenuItem['onClick']) => { + return () => { + onClose(); + callback(); + }; + }; + }, [onClose]); + + const button = ( + + + + ); + + const wrappedItems = useMemo(() => { + return items.map((item, i) => ( + + {item.label} + + )); + }, [items, closeMenuAndCall]); + + return ( + + + + + + + + ); +}; + +const LogEntryContextMenuContent = euiStyled(LogEntryColumnContent)` + overflow: hidden; + user-select: none; +`; + +const AbsoluteWrapper = euiStyled.div` + position: absolute; +`; + +const ButtonWrapper = euiStyled.div` + background: ${(props) => props.theme.eui.euiColorPrimary}; + border-radius: 50%; + padding: 4px; + transform: translateY(-6px); +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx index 0d971151dd95c..2d53203a60e4f 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx @@ -5,6 +5,7 @@ */ import React, { memo, useState, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import { euiStyled } from '../../../../../observability/public'; @@ -18,11 +19,26 @@ import { import { TextScale } from '../../../../common/log_text_scale'; import { LogEntryColumn, LogEntryColumnWidths, iconColumnId } from './log_entry_column'; import { LogEntryFieldColumn } from './log_entry_field_column'; -import { LogEntryActionsColumn } from './log_entry_actions_column'; import { LogEntryMessageColumn } from './log_entry_message_column'; import { LogEntryTimestampColumn } from './log_entry_timestamp_column'; import { monospaceTextStyle, hoveredContentStyle, highlightedContentStyle } from './text_styles'; import { LogEntry, LogColumn } from '../../../../common/http_api'; +import { LogEntryContextMenu } from './log_entry_context_menu'; + +const MENU_LABEL = i18n.translate('xpack.infra.logEntryItemView.logEntryActionsMenuToolTip', { + defaultMessage: 'View actions for line', +}); + +const LOG_DETAILS_LABEL = i18n.translate('xpack.infra.logs.logEntryActionsDetailsButton', { + defaultMessage: 'View details', +}); + +const LOG_VIEW_IN_CONTEXT_LABEL = i18n.translate( + 'xpack.infra.lobs.logEntryActionsViewInContextButton', + { + defaultMessage: 'View in context', + } +); interface LogEntryRowProps { boundingBoxRef?: React.Ref; @@ -76,6 +92,29 @@ export const LogEntryRow = memo( const hasActionViewLogInContext = hasContext && openViewLogInContext !== undefined; const hasActionsMenu = hasActionFlyoutWithItem || hasActionViewLogInContext; + const menuItems = useMemo(() => { + const items = []; + if (hasActionFlyoutWithItem) { + items.push({ + label: LOG_DETAILS_LABEL, + onClick: openFlyout, + }); + } + if (hasActionViewLogInContext) { + items.push({ + label: LOG_VIEW_IN_CONTEXT_LABEL, + onClick: handleOpenViewLogInContext, + }); + } + + return items; + }, [ + hasActionFlyoutWithItem, + hasActionViewLogInContext, + openFlyout, + handleOpenViewLogInContext, + ]); + const logEntryColumnsById = useMemo( () => logEntry.columns.reduce<{ @@ -183,16 +222,15 @@ export const LogEntryRow = memo( key="logColumn iconLogColumn iconLogColumn:details" {...columnWidths[iconColumnId]} > - + {isHovered || isMenuOpen ? ( + + ) : null} ) : null} From 03561745f9138d9f40094e840b36c83dd3a15cf5 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 25 Jun 2020 14:54:18 -0400 Subject: [PATCH 53/82] Remove unused Resolver code (#69914) (#69965) * embeddable * embeddable factory * a file called 'sample' * resolver/index (it was just importing and re-exporting stuff) --- .../public/resolver/embeddable.tsx | 41 - .../public/resolver/factory.ts | 31 - .../public/resolver/index.ts | 8 - .../public/resolver/store/data/sample.ts | 1608 ----------------- 4 files changed, 1688 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/resolver/embeddable.tsx delete mode 100644 x-pack/plugins/security_solution/public/resolver/factory.ts delete mode 100644 x-pack/plugins/security_solution/public/resolver/index.ts delete mode 100644 x-pack/plugins/security_solution/public/resolver/store/data/sample.ts diff --git a/x-pack/plugins/security_solution/public/resolver/embeddable.tsx b/x-pack/plugins/security_solution/public/resolver/embeddable.tsx deleted file mode 100644 index 5ec71e6b3041e..0000000000000 --- a/x-pack/plugins/security_solution/public/resolver/embeddable.tsx +++ /dev/null @@ -1,41 +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 ReactDOM from 'react-dom'; -import React from 'react'; -import { Provider } from 'react-redux'; -import { Resolver } from './view'; -import { storeFactory } from './store'; -import { Embeddable } from '../../../../../src/plugins/embeddable/public'; - -export class ResolverEmbeddable extends Embeddable { - public readonly type = 'resolver'; - private lastRenderTarget?: Element; - - public render(node: HTMLElement) { - if (this.lastRenderTarget !== undefined) { - ReactDOM.unmountComponentAtNode(this.lastRenderTarget); - } - this.lastRenderTarget = node; - const { store } = storeFactory(); - ReactDOM.render( - - - , - node - ); - } - - public reload(): void { - throw new Error('Method not implemented.'); - } - - public destroy(): void { - if (this.lastRenderTarget !== undefined) { - ReactDOM.unmountComponentAtNode(this.lastRenderTarget); - } - } -} diff --git a/x-pack/plugins/security_solution/public/resolver/factory.ts b/x-pack/plugins/security_solution/public/resolver/factory.ts deleted file mode 100644 index 5168d2771e723..0000000000000 --- a/x-pack/plugins/security_solution/public/resolver/factory.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { - IContainer, - EmbeddableInput, - EmbeddableFactoryDefinition, -} from '../../../../../src/plugins/embeddable/public'; -import { ResolverEmbeddable } from './embeddable'; - -export class ResolverEmbeddableFactory implements EmbeddableFactoryDefinition { - public readonly type = 'resolver'; - - public async isEditable() { - return true; - } - - public async create(initialInput: EmbeddableInput, parent?: IContainer) { - return new ResolverEmbeddable(initialInput, {}, parent); - } - - public getDisplayName() { - return i18n.translate('xpack.securitySolution.endpoint.resolver.displayNameTitle', { - defaultMessage: 'Resolver', - }); - } -} diff --git a/x-pack/plugins/security_solution/public/resolver/index.ts b/x-pack/plugins/security_solution/public/resolver/index.ts deleted file mode 100644 index e4f3cc90ae30a..0000000000000 --- a/x-pack/plugins/security_solution/public/resolver/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { ResolverEmbeddableFactory } from './factory'; -export { ResolverEmbeddable } from './embeddable'; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/sample.ts b/x-pack/plugins/security_solution/public/resolver/store/data/sample.ts deleted file mode 100644 index b0ed9f3554c9b..0000000000000 --- a/x-pack/plugins/security_solution/public/resolver/store/data/sample.ts +++ /dev/null @@ -1,1608 +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 { ProcessEvent } from '../../types'; - -interface ProcessEventSampleData { - data: { - result: { - search_results: ProcessEvent[]; - }; - }; -} - -const rawData = { - data: { - code: 200, - result: { - alert_id: 'a9834bf5-42c1-4039-83be-08c3ad3232b3', - bulk_task_id: null, - correlation_id: '7022e509-087e-493d-b02c-d88a206cd993', - created_at: '2019-09-24T03:17:36Z', - endpoint: { - ad_distinguished_name: - 'CN=ENDPOINT-W-1-07,OU=Desktops,OU=Workstations,OU=Computers_DEMO,DC=demo,DC=endgamelabs,DC=net', - ad_hostname: 'demo.endgamelabs.net', - display_operating_system: 'Windows 7 (SP1)', - hostname: 'ENDPOINT-W-1-07', - id: '39153006-0064-424b-99e9-4e21dcc00c2e', - ip_address: '172.31.27.17', - mac_address: '00:50:56:b1:b7:7b', - name: 'ENDPOINT-W-1-07', - operating_system: 'Windows 6.1 Service Pack 1', - status: 'monitored', - updated_at: '2019-09-24T01:48:47.960649+00:00', - }, - event_logging_search_request_count: 3, - family: 'collection', - investigation_id: null, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - message_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - metadata: { - chunk_id: 0, - correlation_id: '7022e509-087e-493d-b02c-d88a206cd993', - final: true, - message_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - origination_task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735', - os_type: 'windows', - priority: 50, - result: { - local_code: 0, - local_msg: 'Success', - }, - semantic_version: '3.52.8', - task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735', - type: 'collection', - }, - origination_task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735', - pagination: { - backwards: false, - eof: false, - page_number: 3, - page_offset: 31666, - params: - 'eyJhbGVydF9pZCI6ICJhOTgzNGJmNS00MmMxLTQwMzktODNiZS0wOGMzYWQzMjMyYjMiLCAidGVtcGxhdGVfZmlsZSI6ICJwcm9jZXNzLWNvbnRleHQubHVhIiwgImNyaXRlcmlhIjogeyJwaWQiOiAxODA4LCAidW5pcXVlX3BpZCI6IDE4OTQzfX0=', - remaining_events: 0, - }, - pending_event_logging_search_request: false, - results_count: 807, - search_results: [ - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - _descendant_count: 6, - command_line: '', - depth: -5, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'already_running', - event_type_full: 'process_event', - integrity_level: 'system', - node_id: 1002, - opcode: 3, - pid: 4, - ppid: 0, - process_name: '', - process_path: '', - serial_event_id: 1002, - timestamp: 132137632670000000, - timestamp_utc: '2019-09-24 01:47:47Z', - unique_pid: 1002, - unique_ppid: 1001, - user_domain: 'NT AUTHORITY', - user_name: 'SYSTEM', - user_sid: 'S-1-5-18', - }, - event_timestamp: 132137632670000000, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - _descendant_count: 5, - command_line: '\\SystemRoot\\System32\\smss.exe', - depth: -4, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'already_running', - event_type_full: 'process_event', - integrity_level: 'system', - md5: '1911a3356fa3f77ccc825ccbac038c2a', - node_id: 1003, - opcode: 3, - original_file_name: 'smss.exe', - pid: 244, - ppid: 4, - process_name: 'smss.exe', - process_path: 'C:\\Windows\\System32\\smss.exe', - serial_event_id: 1003, - sha1: '706473ad489e5365af1e3431c4f8fe80a9139bc2', - sha256: '6ed135b792c81d78b33a57f0f4770db6105c9ed3e2193629cb3ec38bfd5b7e1b', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 1002, - timestamp: 132137632670000000, - timestamp_utc: '2019-09-24 01:47:47Z', - unique_pid: 1003, - unique_ppid: 1002, - user_domain: 'NT AUTHORITY', - user_name: 'SYSTEM', - user_sid: 'S-1-5-18', - }, - event_timestamp: 132137632670000000, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - _descendant_count: 4, - authentication_id: 999, - command_line: '\\SystemRoot\\System32\\smss.exe 00000000 00000048 ', - depth: -3, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'system', - md5: '1911a3356fa3f77ccc825ccbac038c2a', - node_id: 18643, - opcode: 1, - original_file_name: 'smss.exe', - parent_process_name: 'smss.exe', - parent_process_path: 'C:\\Windows\\System32\\smss.exe', - pid: 2364, - ppid: 244, - process_name: 'smss.exe', - process_path: 'C:\\Windows\\System32\\smss.exe', - serial_event_id: 18643, - sha1: '706473ad489e5365af1e3431c4f8fe80a9139bc2', - sha256: '6ed135b792c81d78b33a57f0f4770db6105c9ed3e2193629cb3ec38bfd5b7e1b', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 1003, - timestamp: 132137681960227504, - timestamp_utc: '2019-09-24 03:09:56Z', - unique_pid: 18643, - unique_ppid: 1003, - user_domain: 'NT AUTHORITY', - user_name: 'SYSTEM', - user_sid: 'S-1-5-18', - }, - event_timestamp: 132137681960227504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - _descendant_count: 3, - authentication_id: 999, - command_line: 'winlogon.exe', - depth: -2, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'system', - md5: '1151b1baa6f350b1db6598e0fea7c457', - node_id: 18645, - opcode: 1, - original_file_name: 'WINLOGON.EXE', - parent_process_name: 'smss.exe', - parent_process_path: 'C:\\Windows\\System32\\smss.exe', - pid: 3108, - ppid: 2364, - process_name: 'winlogon.exe', - process_path: 'C:\\Windows\\System32\\winlogon.exe', - serial_event_id: 18645, - sha1: '434856b834baf163c5ea4d26434eeae775a507fb', - sha256: 'b1506e0a7e826eff0f5252ef5026070c46e2235438403a9a24d73ee69c0b8a49', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18643, - timestamp: 132137681961163504, - timestamp_utc: '2019-09-24 03:09:56Z', - unique_pid: 18645, - unique_ppid: 18643, - user_domain: 'NT AUTHORITY', - user_name: 'SYSTEM', - user_sid: 'S-1-5-18', - }, - event_timestamp: 132137681961163504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - depth: -2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: '1911a3356fa3f77ccc825ccbac038c2a', - node_id: 18646, - opcode: 2, - original_file_name: 'smss.exe', - parent_process_name: 'smss.exe', - parent_process_path: 'C:\\Windows\\System32\\smss.exe', - pid: 2364, - ppid: 244, - process_name: 'smss.exe', - process_path: 'C:\\Windows\\System32\\smss.exe', - serial_event_id: 18646, - sha1: '706473ad489e5365af1e3431c4f8fe80a9139bc2', - sha256: '6ed135b792c81d78b33a57f0f4770db6105c9ed3e2193629cb3ec38bfd5b7e1b', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18643, - timestamp: 132137681961787504, - timestamp_utc: '2019-09-24 03:09:56Z', - unique_pid: 18643, - unique_ppid: 1003, - user_domain: 'NT AUTHORITY', - user_name: 'SYSTEM', - user_sid: 'S-1-5-18', - }, - event_timestamp: 132137681961787504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - _descendant_count: 1, - authentication_id: 4904488, - command_line: 'C:\\Windows\\system32\\userinit.exe', - depth: -1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'bafe84e637bf7388c96ef48d4d3fdd53', - node_id: 18833, - opcode: 1, - original_file_name: 'USERINIT.EXE', - parent_process_name: 'winlogon.exe', - parent_process_path: 'C:\\Windows\\System32\\winlogon.exe', - pid: 3560, - ppid: 3108, - process_name: 'userinit.exe', - process_path: 'C:\\Windows\\System32\\userinit.exe', - serial_event_id: 18833, - sha1: '47267f943f060e36604d56c8895a6eece063d9a1', - sha256: '11c194d9adce90027272c627d7fbf3ba5025ff0f7b26a8333f764e11e1382cf9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18645, - timestamp: 132137681981287504, - timestamp_utc: '2019-09-24 03:09:58Z', - unique_pid: 18833, - unique_ppid: 18645, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137681981287504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - _descendant_count: 0, - authentication_id: 4904488, - command_line: 'C:\\Windows\\Explorer.EXE', - depth: 0, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 18943, - opcode: 1, - origin: true, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'userinit.exe', - parent_process_path: 'C:\\Windows\\System32\\userinit.exe', - pid: 1808, - ppid: 3560, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 18943, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18833, - timestamp: 132137681985655504, - timestamp_utc: '2019-09-24 03:09:58Z', - unique_pid: 18943, - unique_ppid: 18833, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137681985655504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe" -n vmusr', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: '8dc5ad50587b936f7f616738112bfd2a', - node_id: 19545, - opcode: 1, - original_file_name: 'vmtoolsd.exe', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3596, - ppid: 1808, - process_name: 'vmtoolsd.exe', - process_path: 'C:\\Program Files\\VMware\\VMware Tools\\vmtoolsd.exe', - serial_event_id: 19545, - sha1: '04479ea30943ec471a6a5ca4c0dc74b5ff496e9f', - sha256: 'd6d9f041da6f724bf69f48bbee3bf41295a0ed4dca715b1908c5f35bc8034d53', - signature_signer: 'VMware, Inc.', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137681999539504, - timestamp_utc: '2019-09-24 03:09:59Z', - unique_pid: 19545, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137681999539504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - depth: 0, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: 'bafe84e637bf7388c96ef48d4d3fdd53', - node_id: 20261, - opcode: 2, - original_file_name: 'USERINIT.EXE', - parent_process_name: 'winlogon.exe', - parent_process_path: 'C:\\Windows\\System32\\winlogon.exe', - pid: 3560, - ppid: 3108, - process_name: 'userinit.exe', - process_path: 'C:\\Windows\\System32\\userinit.exe', - serial_event_id: 20261, - sha1: '47267f943f060e36604d56c8895a6eece063d9a1', - sha256: '11c194d9adce90027272c627d7fbf3ba5025ff0f7b26a8333f764e11e1382cf9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18833, - timestamp: 132137682277819504, - timestamp_utc: '2019-09-24 03:10:27Z', - unique_pid: 18833, - unique_ppid: 18645, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137682277819504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Windows\\explorer.exe" ', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 20303, - opcode: 1, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3124, - ppid: 1808, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 20303, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137682603979504, - timestamp_utc: '2019-09-24 03:11:00Z', - unique_pid: 20303, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137682603979504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: '310624aa-bb2b-442b-a6c9-3284148b0ae3', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 1, - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 20310, - opcode: 2, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3124, - ppid: 1808, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 20310, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 20303, - timestamp: 132137682604229504, - timestamp_utc: '2019-09-24 03:11:00Z', - unique_pid: 20303, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137682604229504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Windows\\explorer.exe" ', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 20455, - opcode: 1, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3084, - ppid: 1808, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 20455, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137682773669504, - timestamp_utc: '2019-09-24 03:11:17Z', - unique_pid: 20455, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137682773669504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 1, - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 20462, - opcode: 2, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3084, - ppid: 1808, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 20462, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 20455, - timestamp: 132137682774259504, - timestamp_utc: '2019-09-24 03:11:17Z', - unique_pid: 20455, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137682774259504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Windows\\System32\\cmd.exe" ', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: '5746bd7e255dd6a8afa06f7c42c1ba41', - node_id: 21120, - opcode: 1, - original_file_name: 'Cmd.Exe', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3280, - ppid: 1808, - process_name: 'cmd.exe', - process_path: 'C:\\Windows\\System32\\cmd.exe', - serial_event_id: 21120, - sha1: '0f3c4ff28f354aede202d54e9d1c5529a3bf87d8', - sha256: 'db06c3534964e3fc79d2763144ba53742d7fa250ca336f4a0fe724b75aaff386', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137682997939504, - timestamp_utc: '2019-09-24 03:11:39Z', - unique_pid: 21120, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137682997939504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Windows\\explorer.exe" ', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 21166, - opcode: 1, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3548, - ppid: 1808, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 21166, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137683166079504, - timestamp_utc: '2019-09-24 03:11:56Z', - unique_pid: 21166, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683166079504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 1, - md5: 'ac4c51eb24aa95b77f705ab159189e24', - node_id: 21173, - opcode: 2, - original_file_name: 'EXPLORER.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3548, - ppid: 1808, - process_name: 'explorer.exe', - process_path: 'C:\\Windows\\explorer.exe', - serial_event_id: 21173, - sha1: '4583daf9442880204730fb2c8a060430640494b1', - sha256: '6a671b92a69755de6fd063fcbe4ba926d83b49f78c42dbaeed8cdb6bbc57576a', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 21166, - timestamp: 132137683166729504, - timestamp_utc: '2019-09-24 03:11:56Z', - unique_pid: 21166, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683166729504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Python27\\python.exe" "C:\\tmp\\dns.py" ', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: '743b91619fbfee3c3e173ba5a17b1290', - node_id: 21480, - opcode: 1, - original_file_name: '', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 4060, - ppid: 1808, - process_name: 'python.exe', - process_path: 'C:\\Python27\\python.exe', - serial_event_id: 21480, - sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620', - sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32', - signature_signer: '', - signature_status: 'noSignature', - source_id: 18943, - timestamp: 132137683493349504, - timestamp_utc: '2019-09-24 03:12:29Z', - unique_pid: 21480, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683493349504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: '743b91619fbfee3c3e173ba5a17b1290', - node_id: 21500, - opcode: 2, - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 4060, - ppid: 1808, - process_name: 'python.exe', - process_path: 'C:\\Python27\\python.exe', - serial_event_id: 21500, - sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620', - sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32', - signature_status: 'noSignature', - source_id: 21480, - timestamp: 132137683493889504, - timestamp_utc: '2019-09-24 03:12:29Z', - unique_pid: 21480, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683493889504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Python27\\python.exe" "C:\\tmp\\dns.py" ', - depth: 2, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: '743b91619fbfee3c3e173ba5a17b1290', - node_id: 21539, - opcode: 1, - original_file_name: '', - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 2888, - ppid: 3280, - process_name: 'python.exe', - process_path: 'C:\\Python27\\python.exe', - serial_event_id: 21539, - sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620', - sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32', - signature_signer: '', - signature_status: 'noSignature', - source_id: 21120, - timestamp: 132137683555889504, - timestamp_utc: '2019-09-24 03:12:35Z', - unique_pid: 21539, - unique_ppid: 21120, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683555889504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 3, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: '743b91619fbfee3c3e173ba5a17b1290', - node_id: 21540, - opcode: 2, - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 2888, - ppid: 3280, - process_name: 'python.exe', - process_path: 'C:\\Python27\\python.exe', - serial_event_id: 21540, - sha1: 'edabcf58d55a5e462f7a368d99616e3ac051c620', - sha256: '45b9384b852d850327e194ac86d84aed8916a3c13fc8f49ca54fddcbca4f7e32', - signature_status: 'noSignature', - source_id: 21539, - timestamp: 132137683556159504, - timestamp_utc: '2019-09-24 03:12:35Z', - unique_pid: 21539, - unique_ppid: 21120, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683556159504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - depth: 2, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'c29675ce0750f73225bf05d03080dfb2', - node_id: 21634, - opcode: 1, - original_file_name: '', - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 3996, - ppid: 3280, - process_name: 'fakenet.exe', - process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - serial_event_id: 21634, - sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', - sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', - signature_signer: '', - signature_status: 'noSignature', - source_id: 21120, - timestamp: 132137683921669504, - timestamp_utc: '2019-09-24 03:13:12Z', - unique_pid: 21634, - unique_ppid: 21120, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683921669504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - depth: 3, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'c29675ce0750f73225bf05d03080dfb2', - node_id: 21669, - opcode: 1, - original_file_name: '', - parent_process_name: 'fakenet.exe', - parent_process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - pid: 184, - ppid: 3996, - process_name: 'fakenet.exe', - process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - serial_event_id: 21669, - sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', - sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', - signature_signer: '', - signature_status: 'noSignature', - source_id: 21634, - timestamp: 132137683923819504, - timestamp_utc: '2019-09-24 03:13:12Z', - unique_pid: 21669, - unique_ppid: 21634, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683923819504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 4, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 1, - md5: 'c29675ce0750f73225bf05d03080dfb2', - node_id: 21679, - opcode: 2, - parent_process_name: 'fakenet.exe', - parent_process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - pid: 184, - ppid: 3996, - process_name: 'fakenet.exe', - process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - serial_event_id: 21679, - sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', - sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', - signature_status: 'noSignature', - source_id: 21669, - timestamp: 132137683931089504, - timestamp_utc: '2019-09-24 03:13:13Z', - unique_pid: 21669, - unique_ppid: 21634, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683931089504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 3, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 1, - md5: 'c29675ce0750f73225bf05d03080dfb2', - node_id: 21694, - opcode: 2, - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 3996, - ppid: 3280, - process_name: 'fakenet.exe', - process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - serial_event_id: 21694, - sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', - sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', - signature_status: 'noSignature', - source_id: 21634, - timestamp: 132137683931569504, - timestamp_utc: '2019-09-24 03:13:13Z', - unique_pid: 21634, - unique_ppid: 21120, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137683931569504, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: - '"C:\\Windows\\system32\\NOTEPAD.EXE" C:\\tmp\\fakenet1.4.3\\configs\\default.ini', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'f2c7bb8acc97f92e987a2d4087d021b1', - node_id: 21769, - opcode: 1, - original_file_name: 'NOTEPAD.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 2492, - ppid: 1808, - process_name: 'notepad.exe', - process_path: 'C:\\Windows\\System32\\notepad.exe', - serial_event_id: 21769, - sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29', - sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137684112851830, - timestamp_utc: '2019-09-24 03:13:31Z', - unique_pid: 21769, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137684112851830, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: 'f2c7bb8acc97f92e987a2d4087d021b1', - node_id: 21794, - opcode: 2, - original_file_name: 'NOTEPAD.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 2492, - ppid: 1808, - process_name: 'notepad.exe', - process_path: 'C:\\Windows\\System32\\notepad.exe', - serial_event_id: 21794, - sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29', - sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 21769, - timestamp: 132137684131573702, - timestamp_utc: '2019-09-24 03:13:33Z', - unique_pid: 21769, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137684131573702, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: 'fakenet.exe', - depth: 2, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'c29675ce0750f73225bf05d03080dfb2', - node_id: 21890, - opcode: 1, - original_file_name: '', - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 1060, - ppid: 3280, - process_name: 'fakenet.exe', - process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - serial_event_id: 21890, - sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', - sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', - signature_signer: '', - signature_status: 'noSignature', - source_id: 21120, - timestamp: 132137684579848525, - timestamp_utc: '2019-09-24 03:14:17Z', - unique_pid: 21890, - unique_ppid: 21120, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137684579848525, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: 'fakenet.exe', - depth: 3, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'c29675ce0750f73225bf05d03080dfb2', - node_id: 21924, - opcode: 1, - original_file_name: '', - parent_process_name: 'fakenet.exe', - parent_process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - pid: 4024, - ppid: 1060, - process_name: 'fakenet.exe', - process_path: 'C:\\tmp\\fakenet1.4.3\\fakenet.exe', - serial_event_id: 21924, - sha1: 'b14763ef982450551bcb09f6e0ecc75d2b9684fb', - sha256: '948f1c024118e434b6867ea593bb180212d35f9d2a9401892903ef22841fb303', - signature_signer: '', - signature_status: 'noSignature', - source_id: 21890, - timestamp: 132137684580468587, - timestamp_utc: '2019-09-24 03:14:18Z', - unique_pid: 21924, - unique_ppid: 21890, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137684580468587, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Windows\\System32\\cmd.exe" ', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: '5746bd7e255dd6a8afa06f7c42c1ba41', - node_id: 22238, - opcode: 1, - original_file_name: 'Cmd.Exe', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 3328, - ppid: 1808, - process_name: 'cmd.exe', - process_path: 'C:\\Windows\\System32\\cmd.exe', - serial_event_id: 22238, - sha1: '0f3c4ff28f354aede202d54e9d1c5529a3bf87d8', - sha256: 'db06c3534964e3fc79d2763144ba53742d7fa250ca336f4a0fe724b75aaff386', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137684944024939, - timestamp_utc: '2019-09-24 03:14:54Z', - unique_pid: 22238, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137684944024939, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - attack_references: [ - { - tactics: ['Privilege Escalation', 'Execution', 'Persistence'], - technique_id: 'T1053', - technique_name: 'Scheduled Task', - }, - ], - authentication_id: 4904488, - command_line: 'SCHTASKS /CREATE /SC MINUTE /TN "Windiws" /TR "C:\\tmp\\scheduler.bat"', - depth: 2, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: '97e0ec3d6d99e8cc2b17ef2d3760e8fc', - node_id: 22376, - opcode: 1, - original_file_name: 'sctasks.exe', - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 2864, - ppid: 3328, - process_name: 'schtasks.exe', - process_path: 'C:\\Windows\\System32\\schtasks.exe', - serial_event_id: 22376, - sha1: 'bd9dceffbcbbc82bee5f2109bd73a57477fe1f92', - sha256: '6dce7d58ebb0d705fcb4179349c441b45e160c94e43934c5ed8fa1964e2cd031', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 22238, - timestamp: 132137685249385472, - timestamp_utc: '2019-09-24 03:15:24Z', - unique_pid: 22376, - unique_ppid: 22238, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137685249385472, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 3, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: '97e0ec3d6d99e8cc2b17ef2d3760e8fc', - node_id: 22384, - opcode: 2, - original_file_name: 'sctasks.exe', - parent_process_name: 'cmd.exe', - parent_process_path: 'C:\\Windows\\System32\\cmd.exe', - pid: 2864, - ppid: 3328, - process_name: 'schtasks.exe', - process_path: 'C:\\Windows\\System32\\schtasks.exe', - serial_event_id: 22384, - sha1: 'bd9dceffbcbbc82bee5f2109bd73a57477fe1f92', - sha256: '6dce7d58ebb0d705fcb4179349c441b45e160c94e43934c5ed8fa1964e2cd031', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 22376, - timestamp: 132137685251515685, - timestamp_utc: '2019-09-24 03:15:25Z', - unique_pid: 22376, - unique_ppid: 22238, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137685251515685, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - authentication_id: 4904488, - command_line: '"C:\\Windows\\System32\\NOTEPAD.EXE" C:\\tmp\\scheduler.bat', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'f2c7bb8acc97f92e987a2d4087d021b1', - node_id: 22448, - opcode: 1, - original_file_name: 'NOTEPAD.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 4048, - ppid: 1808, - process_name: 'notepad.exe', - process_path: 'C:\\Windows\\System32\\notepad.exe', - serial_event_id: 22448, - sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29', - sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137685448755407, - timestamp_utc: '2019-09-24 03:15:44Z', - unique_pid: 22448, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137685448755407, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: 'f2c7bb8acc97f92e987a2d4087d021b1', - node_id: 22464, - opcode: 2, - original_file_name: 'NOTEPAD.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 4048, - ppid: 1808, - process_name: 'notepad.exe', - process_path: 'C:\\Windows\\System32\\notepad.exe', - serial_event_id: 22464, - sha1: '7eb0139d2175739b3ccb0d1110067820be6abd29', - sha256: '142e1d688ef0568370c37187fd9f2351d7ddeda574f8bfa9b0fa4ef42db85aa2', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 22448, - timestamp: 132137685516752206, - timestamp_utc: '2019-09-24 03:15:51Z', - unique_pid: 22448, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137685516752206, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - attack_references: [ - { - tactics: ['Execution'], - technique_id: 'T1085', - technique_name: 'Rundll32', - }, - ], - authentication_id: 4904488, - command_line: - '"C:\\Windows\\system32\\rundll32.exe" C:\\Windows\\system32\\shell32.dll,OpenAs_RunDLL C:\\tmp\\XLS_no_email_Upcoming Events February 2018.xls\\cb85072e6ca66a29cb0b73659a0fe5ba2456d9ba0b52e3a4c89e86549bc6e2c7.xls', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'dd81d91ff3b0763c392422865c9ac12e', - node_id: 22799, - opcode: 1, - original_file_name: 'RUNDLL32.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 2864, - ppid: 1808, - process_name: 'rundll32.exe', - process_path: 'C:\\Windows\\System32\\rundll32.exe', - serial_event_id: 22799, - sha1: '963b55acc8c566876364716d5aafa353995812a8', - sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137686572217742, - timestamp_utc: '2019-09-24 03:17:37Z', - unique_pid: 22799, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137686572217742, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: 'dd81d91ff3b0763c392422865c9ac12e', - node_id: 22805, - opcode: 2, - original_file_name: 'RUNDLL32.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 2864, - ppid: 1808, - process_name: 'rundll32.exe', - process_path: 'C:\\Windows\\System32\\rundll32.exe', - serial_event_id: 22805, - sha1: '963b55acc8c566876364716d5aafa353995812a8', - sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 22799, - timestamp: 132137686585839104, - timestamp_utc: '2019-09-24 03:17:38Z', - unique_pid: 22799, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137686585839104, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - attack_references: [ - { - tactics: ['Execution'], - technique_id: 'T1085', - technique_name: 'Rundll32', - }, - ], - authentication_id: 4904488, - command_line: - '"C:\\Windows\\system32\\rundll32.exe" C:\\Windows\\system32\\shell32.dll,OpenAs_RunDLL C:\\tmp\\Upcoming Defense events February 2018.eml', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'dd81d91ff3b0763c392422865c9ac12e', - node_id: 22933, - opcode: 1, - original_file_name: 'RUNDLL32.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 1864, - ppid: 1808, - process_name: 'rundll32.exe', - process_path: 'C:\\Windows\\System32\\rundll32.exe', - serial_event_id: 22933, - sha1: '963b55acc8c566876364716d5aafa353995812a8', - sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137686702740793, - timestamp_utc: '2019-09-24 03:17:50Z', - unique_pid: 22933, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137686702740793, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: 'dd81d91ff3b0763c392422865c9ac12e', - node_id: 22945, - opcode: 2, - original_file_name: 'RUNDLL32.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 1864, - ppid: 1808, - process_name: 'rundll32.exe', - process_path: 'C:\\Windows\\System32\\rundll32.exe', - serial_event_id: 22945, - sha1: '963b55acc8c566876364716d5aafa353995812a8', - sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 22933, - timestamp: 132137686718432362, - timestamp_utc: '2019-09-24 03:17:51Z', - unique_pid: 22933, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137686718432362, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - attack_references: [ - { - tactics: ['Execution'], - technique_id: 'T1085', - technique_name: 'Rundll32', - }, - ], - authentication_id: 4904488, - command_line: - '"C:\\Windows\\system32\\rundll32.exe" C:\\Windows\\system32\\shell32.dll,OpenAs_RunDLL C:\\Users\\Administrator\\AppData\\Roaming\\Microsoft\\Windows\\SendTo\\Mail Recipient.MAPIMail', - depth: 1, - elevated: true, - elevation_type: 'default', - event_subtype_full: 'creation_event', - event_type_full: 'process_event', - integrity_level: 'high', - md5: 'dd81d91ff3b0763c392422865c9ac12e', - node_id: 27050, - opcode: 1, - original_file_name: 'RUNDLL32.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 568, - ppid: 1808, - process_name: 'rundll32.exe', - process_path: 'C:\\Windows\\System32\\rundll32.exe', - serial_event_id: 27050, - sha1: '963b55acc8c566876364716d5aafa353995812a8', - sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 18943, - timestamp: 132137686926723189, - timestamp_utc: '2019-09-24 03:18:12Z', - unique_pid: 27050, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137686926723189, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - { - collection_id: 'f287067d-3ec7-4d5a-b6de-ea1415bc5e56', - data_buffer: { - depth: 2, - event_subtype_full: 'termination_event', - event_type_full: 'process_event', - exit_code: 0, - md5: 'dd81d91ff3b0763c392422865c9ac12e', - node_id: 27053, - opcode: 2, - original_file_name: 'RUNDLL32.EXE', - parent_process_name: 'explorer.exe', - parent_process_path: 'C:\\Windows\\explorer.exe', - pid: 568, - ppid: 1808, - process_name: 'rundll32.exe', - process_path: 'C:\\Windows\\System32\\rundll32.exe', - serial_event_id: 27053, - sha1: '963b55acc8c566876364716d5aafa353995812a8', - sha256: 'f5691b8f200e3196e6808e932630e862f8f26f31cd949981373f23c9d87db8b9', - signature_signer: 'Microsoft Windows', - signature_status: 'trusted', - source_id: 27050, - timestamp: 132137686939784495, - timestamp_utc: '2019-09-24 03:18:13Z', - unique_pid: 27050, - unique_ppid: 18943, - user_domain: 'ENDPOINT-W-1-07', - user_name: 'vagrant', - user_sid: 'S-1-5-21-3883902650-1642591343-2485142877-1001', - }, - event_timestamp: 132137686939784495, - event_type: 4, - machine_id: '7f1660dc-2c12-ce99-71b8-1ef862aeec34', - serial_event_id: 0, - }, - ], - status: 'success', - task_id: '2bed7d8b-72b1-4650-882c-5167a2fe1735', - total_events_searched: 7730, - type: 'eventLoggingSearchResponse', - }, - }, - metadata: { - count: 39, - next: null, - next_url: null, - per_page: '4000', - previous_url: null, - timestamp: '2019-12-18T19:31:27.565110', - }, -}; - -export const sampleData: ProcessEventSampleData = rawData as ProcessEventSampleData; From 95a60c14f6090e0139bf0c91bab1f31fe178babc Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 25 Jun 2020 20:01:04 +0100 Subject: [PATCH 54/82] [Encrypted Saved Objects] Adds support for migrations in ESO (#69513) (#69977) Introduces migrations into Encrypted Saved Objects. The two main changes here are: 1. The addition of a createMigration api on the EncryptedSavedObjectsPluginSetup. 2. A change in SavedObjects migration to ensure they don't block the event loop. --- src/core/server/mocks.ts | 1 + .../migrations/core/index_migrator.ts | 2 +- .../migrations/core/migrate_raw_docs.test.ts | 4 +- .../migrations/core/migrate_raw_docs.ts | 57 +- x-pack/package.json | 2 +- .../plugins/encrypted_saved_objects/README.md | 132 + .../server/create_migration.test.ts | 296 ++ .../server/create_migration.ts | 91 + .../encrypted_saved_objects_service.mocks.ts | 84 + .../encrypted_saved_objects_service.test.ts | 586 +++- .../crypto/encrypted_saved_objects_service.ts | 149 +- .../server/crypto/index.mock.ts | 69 +- .../server/crypto/index.ts | 1 + .../encrypted_saved_objects/server/mocks.ts | 1 + .../server/plugin.test.ts | 1 + .../encrypted_saved_objects/server/plugin.ts | 35 +- ...ypted_saved_objects_client_wrapper.test.ts | 2 +- .../server/saved_objects/index.test.ts | 2 +- .../config.ts | 8 +- .../api_consumer_plugin/server/index.ts | 96 +- .../encrypted_saved_objects/data.json | 370 +++ .../encrypted_saved_objects/mappings.json | 2413 +++++++++++++++++ .../tests/encrypted_saved_objects_api.ts | 28 + yarn.lock | 5 + 24 files changed, 4281 insertions(+), 154 deletions(-) create mode 100644 x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts create mode 100644 x-pack/plugins/encrypted_saved_objects/server/create_migration.ts create mode 100644 x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts create mode 100644 x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json create mode 100644 x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/mappings.json diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 0770e8843e2f6..2ac5bd98f7ed4 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -46,6 +46,7 @@ export { httpServiceMock } from './http/http_service.mock'; export { loggingSystemMock } from './logging/logging_system.mock'; export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock'; export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; +export { migrationMocks } from './saved_objects/migrations/mocks'; export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { metricsServiceMock } from './metrics/metrics_service.mock'; diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.ts b/src/core/server/saved_objects/migrations/core/index_migrator.ts index b2ffe2ad04a88..e588eb7877322 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.ts @@ -195,7 +195,7 @@ async function migrateSourceToDest(context: Context) { await Index.write( callCluster, dest.indexName, - migrateRawDocs(serializer, documentMigrator.migrate, docs, log) + await migrateRawDocs(serializer, documentMigrator.migrate, docs, log) ); } } diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index e55b72be2436d..6e4dd9615d423 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -26,7 +26,7 @@ import { createSavedObjectsMigrationLoggerMock } from '../../migrations/mocks'; describe('migrateRawDocs', () => { test('converts raw docs to saved objects', async () => { const transform = jest.fn((doc: any) => _.set(doc, 'attributes.name', 'HOI!')); - const result = migrateRawDocs( + const result = await migrateRawDocs( new SavedObjectsSerializer(new SavedObjectTypeRegistry()), transform, [ @@ -55,7 +55,7 @@ describe('migrateRawDocs', () => { const transform = jest.fn((doc: any) => _.set(_.cloneDeep(doc), 'attributes.name', 'TADA') ); - const result = migrateRawDocs( + const result = await migrateRawDocs( new SavedObjectsSerializer(new SavedObjectTypeRegistry()), transform, [ diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts index a2b72ea76c1a2..2bdf59d25dc74 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts @@ -21,7 +21,11 @@ * This file provides logic for migrating raw documents. */ -import { SavedObjectsRawDoc, SavedObjectsSerializer } from '../../serialization'; +import { + SavedObjectsRawDoc, + SavedObjectsSerializer, + SavedObjectUnsanitizedDoc, +} from '../../serialization'; import { TransformFn } from './document_migrator'; import { SavedObjectsMigrationLogger } from '.'; @@ -33,26 +37,51 @@ import { SavedObjectsMigrationLogger } from '.'; * @param {SavedObjectsRawDoc[]} rawDocs * @returns {SavedObjectsRawDoc[]} */ -export function migrateRawDocs( +export async function migrateRawDocs( serializer: SavedObjectsSerializer, migrateDoc: TransformFn, rawDocs: SavedObjectsRawDoc[], log: SavedObjectsMigrationLogger -): SavedObjectsRawDoc[] { - return rawDocs.map((raw) => { +): Promise { + const migrateDocWithoutBlocking = transformNonBlocking(migrateDoc); + const processedDocs = []; + for (const raw of rawDocs) { if (serializer.isRawSavedObject(raw)) { const savedObject = serializer.rawToSavedObject(raw); savedObject.migrationVersion = savedObject.migrationVersion || {}; - return serializer.savedObjectToRaw({ - references: [], - ...migrateDoc(savedObject), - }); + processedDocs.push( + serializer.savedObjectToRaw({ + references: [], + ...(await migrateDocWithoutBlocking(savedObject)), + }) + ); + } else { + log.error( + `Error: Unable to migrate the corrupt Saved Object document ${raw._id}. To prevent Kibana from performing a migration on every restart, please delete or fix this document by ensuring that the namespace and type in the document's id matches the values in the namespace and type fields.`, + { rawDocument: raw } + ); + processedDocs.push(raw); } + } + return processedDocs; +} - log.error( - `Error: Unable to migrate the corrupt Saved Object document ${raw._id}. To prevent Kibana from performing a migration on every restart, please delete or fix this document by ensuring that the namespace and type in the document's id matches the values in the namespace and type fields.`, - { rawDocument: raw } - ); - return raw; - }); +/** + * Migration transform functions are potentially CPU heavy e.g. doing decryption/encryption + * or (de)/serializing large JSON payloads. + * Executing all transforms for a batch in a synchronous loop can block the event-loop for a long time. + * To prevent this we use setImmediate to ensure that the event-loop can process other parallel + * work in between each transform. + */ +function transformNonBlocking( + transform: TransformFn +): (doc: SavedObjectUnsanitizedDoc) => Promise { + // promises aren't enough to unblock the event loop + return (doc: SavedObjectUnsanitizedDoc) => + new Promise((resolve) => { + // set immediate is though + setImmediate(() => { + resolve(transform(doc)); + }); + }); } diff --git a/x-pack/package.json b/x-pack/package.json index d9e00272cbb9a..8765747154cc0 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -199,7 +199,7 @@ "@elastic/eui": "24.1.0", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.3.0", - "@elastic/node-crypto": "1.1.1", + "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.0", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", diff --git a/x-pack/plugins/encrypted_saved_objects/README.md b/x-pack/plugins/encrypted_saved_objects/README.md index 2f0af9e866797..0a5e79a96f02a 100644 --- a/x-pack/plugins/encrypted_saved_objects/README.md +++ b/x-pack/plugins/encrypted_saved_objects/README.md @@ -99,6 +99,138 @@ const savedObjectWithDecryptedContent = await esoClient.getDecryptedAsInternalU one would pass to `SavedObjectsClient.get`. These argument allows to specify `namespace` property that, for example, is required if Saved Object was created within a non-default space. +### Defining migrations +EncryptedSavedObjects rely on standard SavedObject migrations, but due to the additional complexity introduced by the need to decrypt and reencrypt the migrated document, there are some caveats to how we support this. +The good news is, most of this complexity is abstracted away by the plugin and all you need to do is leverage our api. + +The `EncryptedSavedObjects` Plugin _SetupContract_ exposes an `createMigration` api which facilitates defining a migration for your EncryptedSavedObject type. + +The `createMigration` function takes four arguments: + +|Argument|Description|Type| +|---|---|---| +|isMigrationNeededPredicate|A predicate which is called for each document, prior to being decrypted, which confirms whether a document requires migration or not. This predicate is important as the decryption step is costly and we would rather not decrypt and re-encrypt a document if we can avoid it.|function| +|migration|A migration function which will migrate each decrypted document from the old shape to the new one.|function| +|inputType|Optional. An `EncryptedSavedObjectTypeRegistration` which describes the ESOType of the input (the document prior to migration). If this type isn't provided, we'll assume the input doc follows the registered type. |object| +|migratedType| Optional. An `EncryptedSavedObjectTypeRegistration` which describes the ESOType of the output (the document after migration). If this type isn't provided, we'll assume the migrated doc follows the registered type.|object| + +### Example: Migrating a Value + +```typescript +encryptedSavedObjects.registerType({ + type: 'alert', + attributesToEncrypt: new Set(['apiKey']), + attributesToExcludeFromAAD: new Set(['mutedInstanceIds', 'updatedBy']), +}); + +const migration790 = encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return doc.consumer === 'alerting' || doc.consumer === undefined; + }, + (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { + const { + attributes: { consumer }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + consumer: consumer === 'alerting' || !consumer ? 'alerts' : consumer, + }, + }; + } +); +``` + +In the above example you can see thwe following: +1. In `shouldBeMigrated` we limit the migrated alerts to those whose `consumer` field equals `alerting` or is undefined. +2. In the migration function we then migrate the value of `consumer` to the value we want (`alerts` or `unknown`, depending on the current value). In this function we can assume that only documents with a `consumer` of `alerting` or `undefined` will be passed in, but it's still safest not to, and so we use the current `consumer` as the default when needed. +3. Note that we haven't passed in any type definitions. This is because we can rely on the registered type, as the migration is changing a value and not the shape of the object. + +As we said above, an EncryptedSavedObject migration is a normal SavedObjects migration, and so we can plug it into the underlying SavedObject just like any other kind of migration: + +```typescript +savedObjects.registerType({ + name: 'alert', + hidden: true, + namespaceType: 'single', + migrations: { + // apply this migration in 7.9.0 + '7.9.0': migration790, + }, + mappings: { + //... + }, +}); +``` + +### Example: Migating a Type +If your migration needs to change the type by, for example, removing an encrypted field, you will have to specify the legacy type for the input. + +```typescript +encryptedSavedObjects.registerType({ + type: 'alert', + attributesToEncrypt: new Set(['apiKey']), + attributesToExcludeFromAAD: new Set(['mutedInstanceIds', 'updatedBy']), +}); + +const migration790 = encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return doc.consumer === 'alerting' || doc.consumer === undefined; + }, + (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { + const { + attributes: { legacyEncryptedField, ...attributes }, + } = doc; + return { + ...doc, + attributes: { + ...attributes + }, + }; + }, + { + type: 'alert', + attributesToEncrypt: new Set(['apiKey', 'legacyEncryptedField']), + attributesToExcludeFromAAD: new Set(['mutedInstanceIds', 'updatedBy']), + } +); +``` + +As you can see in this example we provide a legacy type which describes the _input_ which needs to be decrypted. +The migration function will default to using the registered type to encrypt the migrated document after the migration is applied. + +If you need to migrate between two legacy types, you can specify both types at once: + +```typescript +encryptedSavedObjects.registerType({ + type: 'alert', + attributesToEncrypt: new Set(['apiKey']), + attributesToExcludeFromAAD: new Set(['mutedInstanceIds', 'updatedBy']), +}); + +const migration780 = encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + // ... + }, + (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { + // ... + }, + // legacy input type + { + type: 'alert', + attributesToEncrypt: new Set(['apiKey', 'legacyEncryptedField']), + attributesToExcludeFromAAD: new Set(['mutedInstanceIds', 'updatedBy']), + }, + // legacy migration type + { + type: 'alert', + attributesToEncrypt: new Set(['apiKey', 'legacyEncryptedField']), + attributesToExcludeFromAAD: new Set(['mutedInstanceIds', 'updatedBy', 'legacyEncryptedField']), + } +); +``` + ## Testing ### Unit tests diff --git a/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts b/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts new file mode 100644 index 0000000000000..620e001677594 --- /dev/null +++ b/x-pack/plugins/encrypted_saved_objects/server/create_migration.test.ts @@ -0,0 +1,296 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { migrationMocks } from 'src/core/server/mocks'; +import { encryptedSavedObjectsServiceMock } from './crypto/index.mock'; +import { getCreateMigration } from './create_migration'; + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('createMigration()', () => { + const { log } = migrationMocks.createContext(); + const inputType = { type: 'known-type-1', attributesToEncrypt: new Set(['firstAttr']) }; + const migrationType = { + type: 'known-type-1', + attributesToEncrypt: new Set(['firstAttr', 'secondAttr']), + }; + + interface InputType { + firstAttr: string; + nonEncryptedAttr?: string; + } + interface MigrationType { + firstAttr: string; + encryptedAttr?: string; + } + + const encryptionSavedObjectService = encryptedSavedObjectsServiceMock.create(); + + it('throws if the types arent compatible', async () => { + const migrationCreator = getCreateMigration(encryptionSavedObjectService, () => + encryptedSavedObjectsServiceMock.create() + ); + expect(() => + migrationCreator( + function (doc): doc is SavedObjectUnsanitizedDoc { + return true; + }, + (doc) => doc, + { + type: 'known-type-1', + attributesToEncrypt: new Set(), + }, + { + type: 'known-type-2', + attributesToEncrypt: new Set(), + } + ) + ).toThrowErrorMatchingInlineSnapshot( + `"An Invalid Encrypted Saved Objects migration is trying to migrate across types (\\"known-type-1\\" => \\"known-type-2\\"), which isn't permitted"` + ); + }); + + describe('migration of an existing type', () => { + it('uses the type in the current service for both input and migration types when none are specified', async () => { + const instantiateServiceWithLegacyType = jest.fn(() => + encryptedSavedObjectsServiceMock.create() + ); + + const migrationCreator = getCreateMigration( + encryptionSavedObjectService, + instantiateServiceWithLegacyType + ); + const noopMigration = migrationCreator( + function (doc): doc is SavedObjectUnsanitizedDoc { + return true; + }, + (doc) => doc + ); + + const attributes = { + firstAttr: 'first_attr', + }; + + encryptionSavedObjectService.decryptAttributesSync.mockReturnValueOnce(attributes); + encryptionSavedObjectService.encryptAttributesSync.mockReturnValueOnce(attributes); + + noopMigration( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + attributes, + }, + { log } + ); + + expect(encryptionSavedObjectService.decryptAttributesSync).toHaveBeenCalledWith( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + }, + attributes + ); + + expect(encryptionSavedObjectService.encryptAttributesSync).toHaveBeenCalledWith( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + }, + attributes + ); + }); + }); + + describe('migration of a single legacy type', () => { + it('uses the input type as the mirgation type when omitted', async () => { + const serviceWithLegacyType = encryptedSavedObjectsServiceMock.create(); + const instantiateServiceWithLegacyType = jest.fn(() => serviceWithLegacyType); + + const migrationCreator = getCreateMigration( + encryptionSavedObjectService, + instantiateServiceWithLegacyType + ); + const noopMigration = migrationCreator( + function (doc): doc is SavedObjectUnsanitizedDoc { + return true; + }, + (doc) => doc, + inputType + ); + + const attributes = { + firstAttr: 'first_attr', + }; + + serviceWithLegacyType.decryptAttributesSync.mockReturnValueOnce(attributes); + encryptionSavedObjectService.encryptAttributesSync.mockReturnValueOnce(attributes); + + noopMigration( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + attributes, + }, + { log } + ); + + expect(serviceWithLegacyType.decryptAttributesSync).toHaveBeenCalledWith( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + }, + attributes + ); + + expect(encryptionSavedObjectService.encryptAttributesSync).toHaveBeenCalledWith( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + }, + attributes + ); + }); + }); + + describe('migration across two legacy types', () => { + const serviceWithInputLegacyType = encryptedSavedObjectsServiceMock.create(); + const serviceWithMigrationLegacyType = encryptedSavedObjectsServiceMock.create(); + const instantiateServiceWithLegacyType = jest.fn(); + + function createMigration() { + instantiateServiceWithLegacyType + .mockImplementationOnce(() => serviceWithInputLegacyType) + .mockImplementationOnce(() => serviceWithMigrationLegacyType); + + const migrationCreator = getCreateMigration( + encryptionSavedObjectService, + instantiateServiceWithLegacyType + ); + return migrationCreator( + function (doc): doc is SavedObjectUnsanitizedDoc { + // migrate doc that have the second field + return ( + typeof (doc as SavedObjectUnsanitizedDoc).attributes.nonEncryptedAttr === + 'string' + ); + }, + ({ attributes: { firstAttr, nonEncryptedAttr }, ...doc }) => ({ + attributes: { + // modify an encrypted field + firstAttr: `~~${firstAttr}~~`, + // encrypt a non encrypted field if it's there + ...(nonEncryptedAttr ? { encryptedAttr: `${nonEncryptedAttr}` } : {}), + }, + ...doc, + }), + inputType, + migrationType + ); + } + + it('doesnt decrypt saved objects that dont need to be migrated', async () => { + const migration = createMigration(); + expect(instantiateServiceWithLegacyType).toHaveBeenCalledWith(inputType); + expect(instantiateServiceWithLegacyType).toHaveBeenCalledWith(migrationType); + + expect( + migration( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + attributes: { + firstAttr: '#####', + }, + }, + { log } + ) + ).toMatchObject({ + id: '123', + type: 'known-type-1', + namespace: 'namespace', + attributes: { + firstAttr: '#####', + }, + }); + + expect(serviceWithInputLegacyType.decryptAttributesSync).not.toHaveBeenCalled(); + expect(serviceWithMigrationLegacyType.encryptAttributesSync).not.toHaveBeenCalled(); + }); + + it('decrypt, migrates and reencrypts saved objects that need to be migrated', async () => { + const migration = createMigration(); + expect(instantiateServiceWithLegacyType).toHaveBeenCalledWith(inputType); + expect(instantiateServiceWithLegacyType).toHaveBeenCalledWith(migrationType); + + serviceWithInputLegacyType.decryptAttributesSync.mockReturnValueOnce({ + firstAttr: 'first_attr', + nonEncryptedAttr: 'non encrypted', + }); + + serviceWithMigrationLegacyType.encryptAttributesSync.mockReturnValueOnce({ + firstAttr: `#####`, + encryptedAttr: `#####`, + }); + + expect( + migration( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + attributes: { + firstAttr: '#####', + nonEncryptedAttr: 'non encrypted', + }, + }, + { log } + ) + ).toMatchObject({ + id: '123', + type: 'known-type-1', + namespace: 'namespace', + attributes: { + firstAttr: '#####', + encryptedAttr: `#####`, + }, + }); + + expect(serviceWithInputLegacyType.decryptAttributesSync).toHaveBeenCalledWith( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + }, + { + firstAttr: '#####', + nonEncryptedAttr: 'non encrypted', + } + ); + + expect(serviceWithMigrationLegacyType.encryptAttributesSync).toHaveBeenCalledWith( + { + id: '123', + type: 'known-type-1', + namespace: 'namespace', + }, + { + firstAttr: `~~first_attr~~`, + encryptedAttr: 'non encrypted', + } + ); + }); + }); +}); diff --git a/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts b/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts new file mode 100644 index 0000000000000..8e9dc1c138966 --- /dev/null +++ b/x-pack/plugins/encrypted_saved_objects/server/create_migration.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + SavedObjectUnsanitizedDoc, + SavedObjectMigrationFn, + SavedObjectMigrationContext, +} from 'src/core/server'; +import { EncryptedSavedObjectTypeRegistration, EncryptedSavedObjectsService } from './crypto'; + +type SavedObjectOptionalMigrationFn = ( + doc: SavedObjectUnsanitizedDoc | SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext +) => SavedObjectUnsanitizedDoc; + +type IsMigrationNeededPredicate = ( + encryptedDoc: + | SavedObjectUnsanitizedDoc + | SavedObjectUnsanitizedDoc +) => encryptedDoc is SavedObjectUnsanitizedDoc; + +export type CreateEncryptedSavedObjectsMigrationFn = < + InputAttributes = unknown, + MigratedAttributes = InputAttributes +>( + isMigrationNeededPredicate: IsMigrationNeededPredicate, + migration: SavedObjectMigrationFn, + inputType?: EncryptedSavedObjectTypeRegistration, + migratedType?: EncryptedSavedObjectTypeRegistration +) => SavedObjectOptionalMigrationFn; + +export const getCreateMigration = ( + encryptedSavedObjectsService: Readonly, + instantiateServiceWithLegacyType: ( + typeRegistration: EncryptedSavedObjectTypeRegistration + ) => EncryptedSavedObjectsService +): CreateEncryptedSavedObjectsMigrationFn => ( + isMigrationNeededPredicate, + migration, + inputType, + migratedType +) => { + if (inputType && migratedType && inputType.type !== migratedType.type) { + throw new Error( + `An Invalid Encrypted Saved Objects migration is trying to migrate across types ("${inputType.type}" => "${migratedType.type}"), which isn't permitted` + ); + } + + const inputService = inputType + ? instantiateServiceWithLegacyType(inputType) + : encryptedSavedObjectsService; + + const migratedService = migratedType + ? instantiateServiceWithLegacyType(migratedType) + : encryptedSavedObjectsService; + + return (encryptedDoc, context) => { + if (!isMigrationNeededPredicate(encryptedDoc)) { + return encryptedDoc; + } + + const descriptor = { + id: encryptedDoc.id!, + type: encryptedDoc.type, + namespace: encryptedDoc.namespace, + }; + + // decrypt the attributes using the input type definition + // then migrate the document + // then encrypt the attributes using the migration type definition + return mapAttributes( + migration( + mapAttributes(encryptedDoc, (inputAttributes) => + inputService.decryptAttributesSync(descriptor, inputAttributes) + ), + context + ), + (migratedAttributes) => + migratedService.encryptAttributesSync(descriptor, migratedAttributes) + ); + }; +}; + +function mapAttributes(obj: SavedObjectUnsanitizedDoc, mapper: (attributes: T) => T) { + return Object.assign(obj, { + attributes: mapper(obj.attributes), + }); +} diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts new file mode 100644 index 0000000000000..c692d8698771f --- /dev/null +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.mocks.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EncryptedSavedObjectsService, + EncryptedSavedObjectTypeRegistration, + SavedObjectDescriptor, +} from './encrypted_saved_objects_service'; + +function createEncryptedSavedObjectsServiceMock() { + return ({ + isRegistered: jest.fn(), + stripOrDecryptAttributes: jest.fn(), + encryptAttributes: jest.fn(), + decryptAttributes: jest.fn(), + encryptAttributesSync: jest.fn(), + decryptAttributesSync: jest.fn(), + } as unknown) as jest.Mocked; +} + +export const encryptedSavedObjectsServiceMock = { + create: createEncryptedSavedObjectsServiceMock, + createWithTypes(registrations: EncryptedSavedObjectTypeRegistration[] = []) { + const mock = createEncryptedSavedObjectsServiceMock(); + + function processAttributes>( + descriptor: Pick, + attrs: T, + action: (attrs: T, attrName: string, shouldExpose: boolean) => void + ) { + const registration = registrations.find((r) => r.type === descriptor.type); + if (!registration) { + return attrs; + } + + const clonedAttrs = { ...attrs }; + for (const attr of registration.attributesToEncrypt) { + const [attrName, shouldExpose] = + typeof attr === 'string' + ? [attr, false] + : [attr.key, attr.dangerouslyExposeValue === true]; + if (attrName in clonedAttrs) { + action(clonedAttrs, attrName, shouldExpose); + } + } + return clonedAttrs; + } + + mock.isRegistered.mockImplementation( + (type) => registrations.findIndex((r) => r.type === type) >= 0 + ); + mock.encryptAttributes.mockImplementation(async (descriptor, attrs) => + processAttributes( + descriptor, + attrs, + (clonedAttrs, attrName) => (clonedAttrs[attrName] = `*${clonedAttrs[attrName]}*`) + ) + ); + mock.decryptAttributes.mockImplementation(async (descriptor, attrs) => + processAttributes( + descriptor, + attrs, + (clonedAttrs, attrName) => + (clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1)) + ) + ); + mock.stripOrDecryptAttributes.mockImplementation((descriptor, attrs) => + Promise.resolve({ + attributes: processAttributes(descriptor, attrs, (clonedAttrs, attrName, shouldExpose) => { + if (shouldExpose) { + clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1); + } else { + delete clonedAttrs[attrName]; + } + }), + }) + ); + + return mock; + }, +}; diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts index db7c96f83dff2..42d2e2ffd1516 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.test.ts @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockAuthenticatedUser } from '../../../security/common/model/authenticated_user.mock'; - -jest.mock('@elastic/node-crypto', () => jest.fn()); +import nodeCrypto, { Crypto } from '@elastic/node-crypto'; +import { mockAuthenticatedUser } from '../../../security/common/model/authenticated_user.mock'; import { EncryptedSavedObjectsAuditLogger } from '../audit'; import { EncryptedSavedObjectsService } from './encrypted_saved_objects_service'; import { EncryptionError } from './encryption_error'; @@ -15,19 +14,37 @@ import { EncryptionError } from './encryption_error'; import { loggingSystemMock } from 'src/core/server/mocks'; import { encryptedSavedObjectsAuditLoggerMock } from '../audit/index.mock'; +const crypto = nodeCrypto({ encryptionKey: 'encryption-key-abc' }); + +const mockNodeCrypto: jest.Mocked = { + encrypt: jest.fn(), + decrypt: jest.fn(), + encryptSync: jest.fn(), + decryptSync: jest.fn(), +}; + let service: EncryptedSavedObjectsService; let mockAuditLogger: jest.Mocked; -beforeEach(() => { - mockAuditLogger = encryptedSavedObjectsAuditLoggerMock.create(); +beforeEach(() => { // Call actual `@elastic/node-crypto` by default, but allow to override implementation in tests. - jest.requireMock('@elastic/node-crypto').mockImplementation((...args: any[]) => { - const { default: nodeCrypto } = jest.requireActual('@elastic/node-crypto'); - return nodeCrypto(...args); - }); + mockNodeCrypto.encrypt.mockImplementation(async (input: any, aad?: string) => + crypto.encrypt(input, aad) + ); + mockNodeCrypto.decrypt.mockImplementation( + async (encryptedOutput: string | Buffer, aad?: string) => crypto.decrypt(encryptedOutput, aad) + ); + mockNodeCrypto.encryptSync.mockImplementation((input: any, aad?: string) => + crypto.encryptSync(input, aad) + ); + mockNodeCrypto.decryptSync.mockImplementation((encryptedOutput: string | Buffer, aad?: string) => + crypto.decryptSync(encryptedOutput, aad) + ); + + mockAuditLogger = encryptedSavedObjectsAuditLoggerMock.create(); service = new EncryptedSavedObjectsService( - 'encryption-key-abc', + mockNodeCrypto, loggingSystemMock.create().get(), mockAuditLogger ); @@ -35,12 +52,6 @@ beforeEach(() => { afterEach(() => jest.resetAllMocks()); -it('correctly initializes crypto', () => { - const mockNodeCrypto = jest.requireMock('@elastic/node-crypto'); - expect(mockNodeCrypto).toHaveBeenCalledTimes(1); - expect(mockNodeCrypto).toHaveBeenCalledWith({ encryptionKey: 'encryption-key-abc' }); -}); - describe('#registerType', () => { it('throws if `attributesToEncrypt` is empty', () => { expect(() => @@ -213,15 +224,13 @@ describe('#stripOrDecryptAttributes', () => { }); describe('#encryptAttributes', () => { - let mockEncrypt: jest.Mock; beforeEach(() => { - mockEncrypt = jest - .fn() - .mockImplementation(async (valueToEncrypt, aad) => `|${valueToEncrypt}|${aad}|`); - jest.requireMock('@elastic/node-crypto').mockReturnValue({ encrypt: mockEncrypt }); + mockNodeCrypto.encrypt.mockImplementation( + async (valueToEncrypt, aad) => `|${valueToEncrypt}|${aad}|` + ); service = new EncryptedSavedObjectsService( - 'encryption-key-abc', + mockNodeCrypto, loggingSystemMock.create().get(), mockAuditLogger ); @@ -399,7 +408,7 @@ describe('#encryptAttributes', () => { attributesToEncrypt: new Set(['attrOne', 'attrThree']), }); - mockEncrypt + mockNodeCrypto.encrypt .mockResolvedValueOnce('Successfully encrypted attrOne') .mockRejectedValueOnce(new Error('Something went wrong with attrThree...')); @@ -915,7 +924,7 @@ describe('#decryptAttributes', () => { it('fails if encrypted with another encryption key', async () => { service = new EncryptedSavedObjectsService( - 'encryption-key-abc*', + nodeCrypto({ encryptionKey: 'encryption-key-abc*' }), loggingSystemMock.create().get(), mockAuditLogger ); @@ -941,3 +950,532 @@ describe('#decryptAttributes', () => { }); }); }); + +describe('#encryptAttributesSync', () => { + beforeEach(() => { + mockNodeCrypto.encryptSync.mockImplementation( + (valueToEncrypt, aad) => `|${valueToEncrypt}|${aad}|` + ); + + service = new EncryptedSavedObjectsService( + mockNodeCrypto, + loggingSystemMock.create().get(), + mockAuditLogger + ); + }); + + it('does not encrypt attributes that are not supposed to be encrypted', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrFour']), + }); + expect( + service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + }); + + it('encrypts only attributes that are supposed to be encrypted', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three', attrFour: null }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree', 'attrFour']), + }); + + expect( + service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toEqual({ + attrOne: '|one|["known-type-1","object-id",{"attrTwo":"two"}]|', + attrTwo: 'two', + attrThree: '|three|["known-type-1","object-id",{"attrTwo":"two"}]|', + attrFour: null, + }); + }); + + it('encrypts only attributes that are supposed to be encrypted even if not all provided', () => { + const attributes = { attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + expect( + service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toEqual({ + attrTwo: 'two', + attrThree: '|three|["known-type-1","object-id",{"attrTwo":"two"}]|', + }); + }); + + it('includes `namespace` into AAD if provided', () => { + const attributes = { attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + expect( + service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + attributes + ) + ).toEqual({ + attrTwo: 'two', + attrThree: '|three|["object-ns","known-type-1","object-id",{"attrTwo":"two"}]|', + }); + }); + + it('does not include specified attributes to AAD', () => { + const knownType1attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const knownType2attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + service.registerType({ + type: 'known-type-2', + attributesToEncrypt: new Set(['attrThree']), + attributesToExcludeFromAAD: new Set(['attrTwo']), + }); + + expect( + service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id-1' }, + knownType1attributes + ) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: '|three|["known-type-1","object-id-1",{"attrOne":"one","attrTwo":"two"}]|', + }); + expect( + service.encryptAttributesSync( + { type: 'known-type-2', id: 'object-id-2' }, + knownType2attributes + ) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: '|three|["known-type-2","object-id-2",{"attrOne":"one"}]|', + }); + }); + + it('encrypts even if no attributes are included into AAD', () => { + const attributes = { attrOne: 'one', attrThree: 'three' }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + expect( + service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id-1' }, attributes) + ).toEqual({ + attrOne: '|one|["known-type-1","object-id-1",{}]|', + attrThree: '|three|["known-type-1","object-id-1",{}]|', + }); + }); + + it('fails if encryption of any attribute fails', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + mockNodeCrypto.encryptSync + .mockImplementationOnce(() => 'Successfully encrypted attrOne') + .mockImplementationOnce(() => { + throw new Error('Something went wrong with attrThree...'); + }); + + expect(() => + service.encryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toThrowError(EncryptionError); + + expect(attributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + }); +}); + +describe('#decryptAttributesSync', () => { + it('does not decrypt attributes that are not supposed to be decrypted', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrFour']), + }); + + expect( + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, attributes) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + }); + + it('decrypts only attributes that are supposed to be decrypted', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three', attrFour: null }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree', 'attrFour']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: expect.not.stringMatching(/^one$/), + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + attrFour: null, + }); + + expect( + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + attrFour: null, + }); + }); + + it('decrypts only attributes that are supposed to be encrypted even if not all provided', () => { + const attributes = { attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + expect( + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes) + ).toEqual({ + attrTwo: 'two', + attrThree: 'three', + }); + }); + + it('decrypts if all attributes that contribute to AAD are present', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + attributesToExcludeFromAAD: new Set(['attrOne']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const attributesWithoutAttr = { attrTwo: 'two', attrThree: encryptedAttributes.attrThree }; + + expect( + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributesWithoutAttr + ) + ).toEqual({ + attrTwo: 'two', + attrThree: 'three', + }); + }); + + it('decrypts even if attributes in AAD are defined in a different order', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + const attributesInDifferentOrder = { + attrThree: encryptedAttributes.attrThree, + attrTwo: 'two', + attrOne: 'one', + }; + + expect( + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributesInDifferentOrder + ) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + }); + + it('decrypts if correct namespace is provided', () => { + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + }); + + expect( + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + encryptedAttributes + ) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + }); + }); + + it('decrypts even if no attributes are included into AAD', () => { + const attributes = { attrOne: 'one', attrThree: 'three' }; + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: expect.not.stringMatching(/^one$/), + attrThree: expect.not.stringMatching(/^three$/), + }); + + expect( + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes) + ).toEqual({ + attrOne: 'one', + attrThree: 'three', + }); + }); + + it('decrypts non-string attributes and restores their original type', () => { + const attributes = { + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + attrFour: null, + attrFive: { nested: 'five' }, + attrSix: 6, + }; + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrOne', 'attrThree', 'attrFour', 'attrFive', 'attrSix']), + }); + + const encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + expect(encryptedAttributes).toEqual({ + attrOne: expect.not.stringMatching(/^one$/), + attrTwo: 'two', + attrThree: expect.not.stringMatching(/^three$/), + attrFour: null, + attrFive: expect.any(String), + attrSix: expect.any(String), + }); + + expect( + service.decryptAttributesSync({ type: 'known-type-1', id: 'object-id' }, encryptedAttributes) + ).toEqual({ + attrOne: 'one', + attrTwo: 'two', + attrThree: 'three', + attrFour: null, + attrFive: { nested: 'five' }, + attrSix: 6, + }); + }); + + describe('decryption failures', () => { + let encryptedAttributes: Record; + + const type1 = { + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }; + + const type2 = { + type: 'known-type-2', + attributesToEncrypt: new Set(['attrThree']), + }; + + beforeEach(() => { + service.registerType(type1); + service.registerType(type2); + + const attributes = { attrOne: 'one', attrTwo: 'two', attrThree: 'three' }; + + encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributes + ); + }); + + it('fails to decrypt if not all attributes that contribute to AAD are present', () => { + const attributesWithoutAttr = { attrTwo: 'two', attrThree: encryptedAttributes.attrThree }; + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + attributesWithoutAttr + ) + ).toThrowError(EncryptionError); + }); + + it('fails to decrypt if ID does not match', () => { + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id*' }, + encryptedAttributes + ) + ).toThrowError(EncryptionError); + }); + + it('fails to decrypt if type does not match', () => { + expect(() => + service.decryptAttributesSync( + { type: 'known-type-2', id: 'object-id' }, + encryptedAttributes + ) + ).toThrowError(EncryptionError); + }); + + it('fails to decrypt if namespace does not match', () => { + encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + { attrOne: 'one', attrTwo: 'two', attrThree: 'three' } + ); + + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-NS' }, + encryptedAttributes + ) + ).toThrowError(EncryptionError); + }); + + it('fails to decrypt if namespace is expected, but is not provided', () => { + encryptedAttributes = service.encryptAttributesSync( + { type: 'known-type-1', id: 'object-id', namespace: 'object-ns' }, + { attrOne: 'one', attrTwo: 'two', attrThree: 'three' } + ); + + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + encryptedAttributes + ) + ).toThrowError(EncryptionError); + }); + + it('fails to decrypt if encrypted attribute is defined, but not a string', () => { + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + { + ...encryptedAttributes, + attrThree: 2, + } + ) + ).toThrowError('Encrypted "attrThree" attribute should be a string, but found number'); + }); + + it('fails to decrypt if encrypted attribute is not correct', () => { + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + { + ...encryptedAttributes, + attrThree: 'some-unknown-string', + } + ) + ).toThrowError(EncryptionError); + }); + + it('fails to decrypt if the AAD attribute has changed', () => { + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + { + ...encryptedAttributes, + attrOne: 'oNe', + } + ) + ).toThrowError(EncryptionError); + }); + + it('fails if encrypted with another encryption key', () => { + service = new EncryptedSavedObjectsService( + nodeCrypto({ encryptionKey: 'encryption-key-abc*' }), + loggingSystemMock.create().get(), + mockAuditLogger + ); + + service.registerType({ + type: 'known-type-1', + attributesToEncrypt: new Set(['attrThree']), + }); + + expect(() => + service.decryptAttributesSync( + { type: 'known-type-1', id: 'object-id' }, + encryptedAttributes + ) + ).toThrowError(EncryptionError); + }); + }); +}); diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts index 5cf3e1c2d65ae..99361107047c2 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/encrypted_saved_objects_service.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import nodeCrypto, { Crypto } from '@elastic/node-crypto'; -import stringify from 'json-stable-stringify'; +import { Crypto, EncryptOutput } from '@elastic/node-crypto'; import typeDetect from 'type-detect'; +import stringify from 'json-stable-stringify'; import { Logger } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/common/model'; import { EncryptedSavedObjectsAuditLogger } from '../audit'; @@ -70,8 +70,6 @@ export function descriptorToArray(descriptor: SavedObjectDescriptor) { * attributes. */ export class EncryptedSavedObjectsService { - private readonly crypto: Readonly; - /** * Map of all registered saved object types where the `key` is saved object type and the `value` * is the definition (names of attributes that need to be encrypted etc.). @@ -82,17 +80,15 @@ export class EncryptedSavedObjectsService { > = new Map(); /** - * @param encryptionKey The key used to encrypt and decrypt saved objects attributes. + * @param crypto nodeCrypto instance. * @param logger Ordinary logger instance. * @param audit Audit logger instance. */ constructor( - encryptionKey: string, + private readonly crypto: Readonly, private readonly logger: Logger, private readonly audit: EncryptedSavedObjectsAuditLogger - ) { - this.crypto = nodeCrypto({ encryptionKey }); - } + ) {} /** * Registers saved object type as the one that contains attributes that should be encrypted. @@ -193,20 +189,11 @@ export class EncryptedSavedObjectsService { return { attributes: clonedAttributes as T, error: decryptionError }; } - /** - * Takes saved object attributes for the specified type and encrypts all of them that are supposed - * to be encrypted if any and returns that __NEW__ attributes dictionary back. If none of the - * attributes were encrypted original attributes dictionary is returned. - * @param descriptor Descriptor of the saved object to encrypt attributes for. - * @param attributes Dictionary of __ALL__ saved object attributes. - * @param [params] Additional parameters. - * @throws Will throw if encryption fails for whatever reason. - */ - public async encryptAttributes>( + private *attributesToEncryptIterator>( descriptor: SavedObjectDescriptor, attributes: T, params?: CommonParameters - ): Promise { + ): Iterator<[unknown, string], T, string> { const typeDefinition = this.typeDefinitions.get(descriptor.type); if (typeDefinition === undefined) { return attributes; @@ -218,10 +205,7 @@ export class EncryptedSavedObjectsService { const attributeValue = attributes[attributeName]; if (attributeValue != null) { try { - encryptedAttributes[attributeName] = await this.crypto.encrypt( - attributeValue, - encryptionAAD - ); + encryptedAttributes[attributeName] = (yield [attributeValue, encryptionAAD])!; } catch (err) { this.logger.error( `Failed to encrypt "${attributeName}" attribute: ${err.message || err}` @@ -263,6 +247,64 @@ export class EncryptedSavedObjectsService { }; } + /** + * Takes saved object attributes for the specified type and encrypts all of them that are supposed + * to be encrypted if any and returns that __NEW__ attributes dictionary back. If none of the + * attributes were encrypted original attributes dictionary is returned. + * @param descriptor Descriptor of the saved object to encrypt attributes for. + * @param attributes Dictionary of __ALL__ saved object attributes. + * @param [params] Additional parameters. + * @throws Will throw if encryption fails for whatever reason. + */ + public async encryptAttributes>( + descriptor: SavedObjectDescriptor, + attributes: T, + params?: CommonParameters + ): Promise { + const iterator = this.attributesToEncryptIterator(descriptor, attributes, params); + + let iteratorResult = iterator.next(); + while (!iteratorResult.done) { + const [attributeValue, encryptionAAD] = iteratorResult.value; + try { + iteratorResult = iterator.next(await this.crypto.encrypt(attributeValue, encryptionAAD)); + } catch (err) { + iterator.throw!(err); + } + } + + return iteratorResult.value; + } + + /** + * Takes saved object attributes for the specified type and encrypts all of them that are supposed + * to be encrypted if any and returns that __NEW__ attributes dictionary back. If none of the + * attributes were encrypted original attributes dictionary is returned. + * @param descriptor Descriptor of the saved object to encrypt attributes for. + * @param attributes Dictionary of __ALL__ saved object attributes. + * @param [params] Additional parameters. + * @throws Will throw if encryption fails for whatever reason. + */ + public encryptAttributesSync>( + descriptor: SavedObjectDescriptor, + attributes: T, + params?: CommonParameters + ): T { + const iterator = this.attributesToEncryptIterator(descriptor, attributes, params); + + let iteratorResult = iterator.next(); + while (!iteratorResult.done) { + const [attributeValue, encryptionAAD] = iteratorResult.value; + try { + iteratorResult = iterator.next(this.crypto.encryptSync(attributeValue, encryptionAAD)); + } catch (err) { + iterator.throw!(err); + } + } + + return iteratorResult.value; + } + /** * Takes saved object attributes for the specified type and decrypts all of them that are supposed * to be encrypted if any and returns that __NEW__ attributes dictionary back. If none of the @@ -278,13 +320,65 @@ export class EncryptedSavedObjectsService { attributes: T, params?: CommonParameters ): Promise { + const iterator = this.attributesToDecryptIterator(descriptor, attributes, params); + + let iteratorResult = iterator.next(); + while (!iteratorResult.done) { + const [attributeValue, encryptionAAD] = iteratorResult.value; + try { + iteratorResult = iterator.next( + (await this.crypto.decrypt(attributeValue, encryptionAAD)) as string + ); + } catch (err) { + iterator.throw!(err); + } + } + + return iteratorResult.value; + } + + /** + * Takes saved object attributes for the specified type and decrypts all of them that are supposed + * to be encrypted if any and returns that __NEW__ attributes dictionary back. If none of the + * attributes were decrypted original attributes dictionary is returned. + * @param descriptor Descriptor of the saved object to decrypt attributes for. + * @param attributes Dictionary of __ALL__ saved object attributes. + * @param [params] Additional parameters. + * @throws Will throw if decryption fails for whatever reason. + * @throws Will throw if any of the attributes to decrypt is not a string. + */ + public decryptAttributesSync>( + descriptor: SavedObjectDescriptor, + attributes: T, + params?: CommonParameters + ): T { + const iterator = this.attributesToDecryptIterator(descriptor, attributes, params); + + let iteratorResult = iterator.next(); + while (!iteratorResult.done) { + const [attributeValue, encryptionAAD] = iteratorResult.value; + try { + iteratorResult = iterator.next(this.crypto.decryptSync(attributeValue, encryptionAAD)); + } catch (err) { + iterator.throw!(err); + } + } + + return iteratorResult.value; + } + + private *attributesToDecryptIterator>( + descriptor: SavedObjectDescriptor, + attributes: T, + params?: CommonParameters + ): Iterator<[string, string], T, EncryptOutput> { const typeDefinition = this.typeDefinitions.get(descriptor.type); if (typeDefinition === undefined) { return attributes; } const encryptionAAD = this.getAAD(typeDefinition, descriptor, attributes); - const decryptedAttributes: Record = {}; + const decryptedAttributes: Record = {}; for (const attributeName of typeDefinition.attributesToEncrypt) { const attributeValue = attributes[attributeName]; if (attributeValue == null) { @@ -301,10 +395,7 @@ export class EncryptedSavedObjectsService { } try { - decryptedAttributes[attributeName] = (await this.crypto.decrypt( - attributeValue, - encryptionAAD - )) as string; + decryptedAttributes[attributeName] = (yield [attributeValue, encryptionAAD])!; } catch (err) { this.logger.error(`Failed to decrypt "${attributeName}" attribute: ${err.message || err}`); this.audit.decryptAttributeFailure(attributeName, descriptor, params?.user); diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/index.mock.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/index.mock.ts index 11a0cd6f33307..3e4983deca625 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/index.mock.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/index.mock.ts @@ -4,71 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EncryptedSavedObjectsService, - EncryptedSavedObjectTypeRegistration, - SavedObjectDescriptor, -} from '.'; - -export const encryptedSavedObjectsServiceMock = { - create(registrations: EncryptedSavedObjectTypeRegistration[] = []) { - const mock: jest.Mocked = new (jest.requireMock( - './encrypted_saved_objects_service' - ).EncryptedSavedObjectsService)(); - - function processAttributes>( - descriptor: Pick, - attrs: T, - action: (attrs: T, attrName: string, shouldExpose: boolean) => void - ) { - const registration = registrations.find((r) => r.type === descriptor.type); - if (!registration) { - return attrs; - } - - const clonedAttrs = { ...attrs }; - for (const attr of registration.attributesToEncrypt) { - const [attrName, shouldExpose] = - typeof attr === 'string' - ? [attr, false] - : [attr.key, attr.dangerouslyExposeValue === true]; - if (attrName in clonedAttrs) { - action(clonedAttrs, attrName, shouldExpose); - } - } - return clonedAttrs; - } - - mock.isRegistered.mockImplementation( - (type) => registrations.findIndex((r) => r.type === type) >= 0 - ); - mock.encryptAttributes.mockImplementation(async (descriptor, attrs) => - processAttributes( - descriptor, - attrs, - (clonedAttrs, attrName) => (clonedAttrs[attrName] = `*${clonedAttrs[attrName]}*`) - ) - ); - mock.decryptAttributes.mockImplementation(async (descriptor, attrs) => - processAttributes( - descriptor, - attrs, - (clonedAttrs, attrName) => - (clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1)) - ) - ); - mock.stripOrDecryptAttributes.mockImplementation((descriptor, attrs) => - Promise.resolve({ - attributes: processAttributes(descriptor, attrs, (clonedAttrs, attrName, shouldExpose) => { - if (shouldExpose) { - clonedAttrs[attrName] = (clonedAttrs[attrName] as string).slice(1, -1); - } else { - delete clonedAttrs[attrName]; - } - }), - }) - ); - - return mock; - }, -}; +export { encryptedSavedObjectsServiceMock } from './encrypted_saved_objects_service.mocks'; diff --git a/x-pack/plugins/encrypted_saved_objects/server/crypto/index.ts b/x-pack/plugins/encrypted_saved_objects/server/crypto/index.ts index 0849f0eb320dd..75445bd24eba8 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/crypto/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/crypto/index.ts @@ -11,3 +11,4 @@ export { SavedObjectDescriptor, } from './encrypted_saved_objects_service'; export { EncryptionError } from './encryption_error'; +export { EncryptedSavedObjectAttributesDefinition } from './encrypted_saved_object_type_definition'; diff --git a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts index 38ac8f254315e..adec3a3b9fbf4 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts @@ -12,6 +12,7 @@ function createEncryptedSavedObjectsSetupMock() { registerType: jest.fn(), __legacyCompat: { registerLegacyAPI: jest.fn() }, usingEphemeralEncryptionKey: true, + createMigration: jest.fn(), } as jest.Mocked; } diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts index 4afd74488f9fe..57108954f2568 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.test.ts @@ -16,6 +16,7 @@ describe('EncryptedSavedObjects Plugin', () => { await expect(plugin.setup(coreMock.createSetup(), { security: securityMock.createSetup() })) .resolves.toMatchInlineSnapshot(` Object { + "createMigration": [Function], "registerType": [Function], "usingEphemeralEncryptionKey": true, } diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index cdbdd18b9d696..69777798ddf19 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import nodeCrypto from '@elastic/node-crypto'; import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; import { first } from 'rxjs/operators'; import { SecurityPluginSetup } from '../../security/server'; @@ -15,6 +16,7 @@ import { } from './crypto'; import { EncryptedSavedObjectsAuditLogger } from './audit'; import { setupSavedObjects, ClientInstanciator } from './saved_objects'; +import { getCreateMigration, CreateEncryptedSavedObjectsMigrationFn } from './create_migration'; export interface PluginsSetup { security?: SecurityPluginSetup; @@ -23,6 +25,7 @@ export interface PluginsSetup { export interface EncryptedSavedObjectsPluginSetup { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void; usingEphemeralEncryptionKey: boolean; + createMigration: CreateEncryptedSavedObjectsMigrationFn; } export interface EncryptedSavedObjectsPluginStart { @@ -45,18 +48,18 @@ export class Plugin { core: CoreSetup, deps: PluginsSetup ): Promise { - const { config, usingEphemeralEncryptionKey } = await createConfig$(this.initializerContext) - .pipe(first()) - .toPromise(); + const { + config: { encryptionKey }, + usingEphemeralEncryptionKey, + } = await createConfig$(this.initializerContext).pipe(first()).toPromise(); + + const crypto = nodeCrypto({ encryptionKey }); + const auditLogger = new EncryptedSavedObjectsAuditLogger( + deps.security?.audit.getLogger('encryptedSavedObjects') + ); const service = Object.freeze( - new EncryptedSavedObjectsService( - config.encryptionKey, - this.logger, - new EncryptedSavedObjectsAuditLogger( - deps.security?.audit.getLogger('encryptedSavedObjects') - ) - ) + new EncryptedSavedObjectsService(crypto, this.logger, auditLogger) ); this.savedObjectsSetup = setupSavedObjects({ @@ -70,6 +73,18 @@ export class Plugin { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => service.registerType(typeRegistration), usingEphemeralEncryptionKey, + createMigration: getCreateMigration( + service, + (typeRegistration: EncryptedSavedObjectTypeRegistration) => { + const serviceForMigration = new EncryptedSavedObjectsService( + crypto, + this.logger, + auditLogger + ); + serviceForMigration.registerType(typeRegistration); + return serviceForMigration; + } + ), }; } diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index ec5d81532e238..eea19bb1aa7dd 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -22,7 +22,7 @@ let encryptedSavedObjectsServiceMockInstance: jest.Mocked { mockBaseClient = savedObjectsClientMock.create(); mockBaseTypeRegistry = savedObjectsTypeRegistryMock.create(); - encryptedSavedObjectsServiceMockInstance = encryptedSavedObjectsServiceMock.create([ + encryptedSavedObjectsServiceMockInstance = encryptedSavedObjectsServiceMock.createWithTypes([ { type: 'known-type', attributesToEncrypt: new Set([ diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.test.ts index 8e9f12268cd7e..ef9aed8706e2c 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/index.test.ts @@ -42,7 +42,7 @@ describe('#setupSavedObjects', () => { coreSetupMock = coreMock.createSetup(); coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}, {}]); - mockEncryptedSavedObjectsService = encryptedSavedObjectsServiceMock.create([ + mockEncryptedSavedObjectsService = encryptedSavedObjectsServiceMock.createWithTypes([ { type: 'known-type', attributesToEncrypt: new Set(['attrSecret']) }, ]); setupContract = setupSavedObjects({ diff --git a/x-pack/test/encrypted_saved_objects_api_integration/config.ts b/x-pack/test/encrypted_saved_objects_api_integration/config.ts index fb643c2c5a901..f061a38b72ce6 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/config.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve } from 'path'; +import path from 'path'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; @@ -18,12 +18,16 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { junit: { reportName: 'X-Pack Encrypted Saved Objects API Integration Tests', }, + esArchiver: { + directory: path.join(__dirname, 'fixtures', 'es_archiver'), + }, esTestCluster: xPackAPITestsConfig.get('esTestCluster'), kbnTestServer: { ...xPackAPITestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), - `--plugin-path=${resolve(__dirname, './fixtures/api_consumer_plugin')}`, + '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', + `--plugin-path=${path.resolve(__dirname, './fixtures/api_consumer_plugin')}`, ], }, }; diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts index 7fb4de9ae4dc1..87bed7f416019 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts @@ -9,6 +9,7 @@ import { CoreSetup, PluginInitializer, SavedObjectsNamespaceType, + SavedObjectUnsanitizedDoc, } from '../../../../../../src/core/server'; import { EncryptedSavedObjectsPluginSetup, @@ -23,6 +24,17 @@ const SAVED_OBJECT_WITH_SECRET_AND_MULTIPLE_SPACES_TYPE = 'saved-object-with-secret-and-multiple-spaces'; const SAVED_OBJECT_WITHOUT_SECRET_TYPE = 'saved-object-without-secret'; +const SAVED_OBJECT_WITH_MIGRATION_TYPE = 'saved-object-with-migration'; +interface MigratedTypePre790 { + nonEncryptedAttribute: string; + encryptedAttribute: string; +} +interface MigratedType { + nonEncryptedAttribute: string; + encryptedAttribute: string; + additionalEncryptedAttribute: string; +} + export interface PluginsSetup { encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; spaces: SpacesPluginSetup; @@ -34,7 +46,7 @@ export interface PluginsStart { } export const plugin: PluginInitializer = () => ({ - setup(core: CoreSetup, deps) { + setup(core: CoreSetup, deps: PluginsSetup) { for (const [name, namespaceType, hidden] of [ [SAVED_OBJECT_WITH_SECRET_TYPE, 'single', false], [HIDDEN_SAVED_OBJECT_WITH_SECRET_TYPE, 'single', true], @@ -71,6 +83,8 @@ export const plugin: PluginInitializer = mappings: deepFreeze({ properties: { publicProperty: { type: 'keyword' } } }), }); + defineTypeWithMigration(core, deps); + const router = core.http.createRouter(); router.get( { @@ -103,3 +117,83 @@ export const plugin: PluginInitializer = start() {}, stop() {}, }); + +function defineTypeWithMigration(core: CoreSetup, deps: PluginsSetup) { + const typePriorTo790 = { + type: SAVED_OBJECT_WITH_MIGRATION_TYPE, + attributesToEncrypt: new Set(['encryptedAttribute']), + }; + + // current type is registered + deps.encryptedSavedObjects.registerType({ + type: SAVED_OBJECT_WITH_MIGRATION_TYPE, + attributesToEncrypt: new Set(['encryptedAttribute', 'additionalEncryptedAttribute']), + }); + + core.savedObjects.registerType({ + name: SAVED_OBJECT_WITH_MIGRATION_TYPE, + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + nonEncryptedAttribute: { + type: 'keyword', + }, + encryptedAttribute: { + type: 'binary', + }, + additionalEncryptedAttribute: { + type: 'keyword', + }, + }, + }, + migrations: { + // in this version we migrated a non encrypted field and type didnt change + '7.8.0': deps.encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return true; + }, + ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectUnsanitizedDoc => { + const { + attributes: { nonEncryptedAttribute }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + nonEncryptedAttribute: `${nonEncryptedAttribute}-migrated`, + }, + }; + }, + // type hasn't changed as the field we're updating is not an encrypted one + typePriorTo790, + typePriorTo790 + ), + // in this version we encrypted an existing non encrypted field + '7.9.0': deps.encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return true; + }, + ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectUnsanitizedDoc => { + const { + attributes: { nonEncryptedAttribute }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + nonEncryptedAttribute, + // clone and modify the non encrypted field + additionalEncryptedAttribute: `${nonEncryptedAttribute}-encrypted`, + }, + }; + }, + typePriorTo790 + ), + }, + }); +} diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json new file mode 100644 index 0000000000000..88ec54cdf3a54 --- /dev/null +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/data.json @@ -0,0 +1,370 @@ +{ + "type": "doc", + "value": { + "id": "config:8.0.0", + "index": ".kibana_1", + "source": { + "config": { + "buildNum": 9007199254740991 + }, + "migrationVersion": { + "config": "7.9.0" + }, + "references": [ + ], + "type": "config", + "updated_at": "2020-06-17T15:03:14.532Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "space": "6.6.0" + }, + "references": [ + ], + "space": { + "_reserved": true, + "color": "#00bfb3", + "description": "This is your default space!", + "disabledFeatures": [ + ], + "name": "Default" + }, + "type": "space", + "updated_at": "2020-06-17T15:03:27.426Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "apm-telemetry:apm-telemetry", + "index": ".kibana_1", + "source": { + "apm-telemetry": { + "agents": { + }, + "cardinality": { + "transaction": { + "name": { + "all_agents": { + "1d": 0 + }, + "rum": { + "1d": 0 + } + } + }, + "user_agent": { + "original": { + "all_agents": { + "1d": 0 + }, + "rum": { + "1d": 0 + } + } + } + }, + "counts": { + "agent_configuration": { + "all": 0 + }, + "error": { + "1d": 0, + "all": 0 + }, + "max_error_groups_per_service": { + "1d": 0 + }, + "max_transaction_groups_per_service": { + "1d": 0 + }, + "metric": { + "1d": 0, + "all": 0 + }, + "onboarding": { + "1d": 0, + "all": 0 + }, + "services": { + "1d": 0 + }, + "sourcemap": { + "1d": 0, + "all": 0 + }, + "span": { + "1d": 0, + "all": 0 + }, + "traces": { + "1d": 0 + }, + "transaction": { + "1d": 0, + "all": 0 + } + }, + "has_any_services": false, + "indices": { + "all": { + "total": { + "docs": { + "count": 0 + }, + "store": { + "size_in_bytes": 416 + } + } + }, + "shards": { + "total": 2 + } + }, + "integrations": { + "ml": { + "all_jobs_count": 0 + } + }, + "services_per_agent": { + "dotnet": 0, + "go": 0, + "java": 0, + "js-base": 0, + "nodejs": 0, + "python": 0, + "ruby": 0, + "rum-js": 0 + }, + "tasks": { + "agent_configuration": { + "took": { + "ms": 21 + } + }, + "agents": { + "took": { + "ms": 65 + } + }, + "cardinality": { + "took": { + "ms": 80 + } + }, + "groupings": { + "took": { + "ms": 25 + } + }, + "indices_stats": { + "took": { + "ms": 65 + } + }, + "integrations": { + "took": { + "ms": 108 + } + }, + "processor_events": { + "took": { + "ms": 113 + } + }, + "services": { + "took": { + "ms": 98 + } + }, + "versions": { + "took": { + "ms": 6 + } + } + } + }, + "references": [ + ], + "type": "apm-telemetry", + "updated_at": "2020-06-17T15:03:47.184Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "saved-object-with-migration:74f3e6d7-b7bb-477d-ac28-92ee22728e6e", + "index": ".kibana_1", + "source": { + "saved-object-with-migration": { + "encryptedAttribute": "JuDwwSjflpKmPKUIfjgo04E0DW9iyhp8C94hwvflgkS0SUUPt+862FQ1eja4VEfEG7HVUt7xxj+BWeZv9vrf4olxgbr4/f5RrT8BVic0EOVS9nhspiDVEv12mV0uDWGtdneB/UWyaZg+0Qr0tPrwceSl8BS///U=", + "nonEncryptedAttribute": "elastic" + }, + "migrationVersion": { + "saved-object-with-migration": "7.7.0" + }, + "references": [ + ], + "type": "saved-object-with-migration", + "updated_at": "2020-06-17T15:35:39.839Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:5f01fd40-b0b0-11ea-9510-fdf248d5f2a4", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "management", + "minutesOnScreen": 1.60245, + "numberOfClicks": 6, + "timestamp": "2020-06-17T15:36:54.292Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-06-17T15:36:54.292Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "application_usage_transactional:4ca5ac00-b0b0-11ea-9510-fdf248d5f2a4", + "index": ".kibana_1", + "source": { + "application_usage_transactional": { + "appId": "home", + "minutesOnScreen": 0.4106666666666667, + "numberOfClicks": 3, + "timestamp": "2020-06-17T15:36:23.487Z" + }, + "references": [ + ], + "type": "application_usage_transactional", + "updated_at": "2020-06-17T15:36:23.488Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:kibana-user_agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", + "index": ".kibana_1", + "source": { + "references": [ + ], + "type": "ui-metric", + "ui-metric": { + "count": 1 + }, + "updated_at": "2020-06-17T15:36:23.487Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:Kibana_home:sampleDataDecline", + "index": ".kibana_1", + "source": { + "type": "ui-metric", + "ui-metric": { + "count": 1 + }, + "updated_at": "2020-06-17T15:36:23.488Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:Kibana_home:welcomeScreenMount", + "index": ".kibana_1", + "source": { + "type": "ui-metric", + "ui-metric": { + "count": 1 + }, + "updated_at": "2020-06-17T15:36:23.488Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "telemetry:telemetry", + "index": ".kibana_1", + "source": { + "references": [ + ], + "telemetry": { + "lastReported": 1592408310031, + "reportFailureCount": 0, + "userHasSeenNotice": true + }, + "type": "telemetry", + "updated_at": "2020-06-17T15:38:30.031Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "maps-telemetry:maps-telemetry", + "index": ".kibana_1", + "source": { + "maps-telemetry": { + "attributesPerMap": { + "dataSourcesCount": { + "avg": 0, + "max": 0, + "min": 0 + }, + "emsVectorLayersCount": { + }, + "layerTypesCount": { + }, + "layersCount": { + "avg": 0, + "max": 0, + "min": 0 + } + }, + "indexPatternsWithGeoFieldCount": 0, + "indexPatternsWithGeoPointFieldCount": 0, + "indexPatternsWithGeoShapeFieldCount": 0, + "mapsTotalCount": 0, + "settings": { + "showMapVisualizationTypes": false + }, + "timeCaptured": "2020-06-17T16:29:27.563Z" + }, + "references": [ + ], + "type": "maps-telemetry", + "updated_at": "2020-06-17T16:29:27.563Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/mappings.json b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/mappings.json new file mode 100644 index 0000000000000..c025ad9da1a9c --- /dev/null +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/es_archiver/encrypted_saved_objects/mappings.json @@ -0,0 +1,2413 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", + "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "todo": "082a2cc96a590268344d5cd74c159ac4", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "saved-object-with-migration": { + "properties": { + "encryptedAttribute": { + "type": "binary" + }, + "nonEncryptedAttribute": { + "type": "keyword" + }, + "additionalEncryptedAttribute": { + "type": "binary" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "properties": { + "agents": { + "properties": { + "dotnet": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "go": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "java": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "js-base": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "nodejs": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "python": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "ruby": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "rum-js": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "inventory-view": { + "properties": { + "accountId": { + "type": "keyword" + }, + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "legend": { + "properties": { + "palette": { + "type": "keyword" + }, + "reverseColors": { + "type": "boolean" + }, + "steps": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "region": { + "type": "keyword" + }, + "sort": { + "properties": { + "by": { + "type": "keyword" + }, + "direction": { + "type": "keyword" + } + } + }, + "time": { + "type": "long" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, + "indexPatternsWithGeoPointFieldCount": { + "type": "long" + }, + "indexPatternsWithGeoShapeFieldCount": { + "type": "long" + }, + "mapsTotalCount": { + "type": "long" + }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "forceInterval": { + "type": "boolean" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "alert": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "saved-object-with-migration": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "todo": { + "properties": { + "icon": { + "type": "keyword" + }, + "task": { + "type": "text" + }, + "title": { + "type": "keyword" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "type": "keyword" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "integer" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "type": "keyword" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "type": "keyword" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "certAgeThreshold": { + "type": "long" + }, + "certExpirationThreshold": { + "type": "long" + }, + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts index 774f6b3baf393..9303280bbc258 100644 --- a/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts @@ -12,6 +12,7 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const randomness = getService('randomness'); const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret'; const HIDDEN_SAVED_OBJECT_WITH_SECRET_TYPE = 'hidden-saved-object-with-secret'; @@ -502,5 +503,32 @@ export default function ({ getService }: FtrProviderContext) { ); }); }); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('encrypted_saved_objects'); + }); + + after(async () => { + await esArchiver.unload('encrypted_saved_objects'); + }); + + it('migrates unencrypted fields on saved objects', async () => { + const { body: decryptedResponse } = await supertest + .get( + `/api/saved_objects/get-decrypted-as-internal-user/saved-object-with-migration/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` + ) + .expect(200); + + expect(decryptedResponse.attributes).to.eql({ + // ensures the encrypted field can still be decrypted after the migration + encryptedAttribute: 'this is my secret api key', + // ensures the non-encrypted field has been migrated in 7.8.0 + nonEncryptedAttribute: 'elastic-migrated', + // ensures the non-encrypted field has been migrated into a new encrypted field in 7.9.0 + additionalEncryptedAttribute: 'elastic-migrated-encrypted', + }); + }); + }); }); } diff --git a/yarn.lock b/yarn.lock index 4715703e86b83..e87e7b77f0979 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2361,6 +2361,11 @@ resolved "https://registry.yarnpkg.com/@elastic/node-crypto/-/node-crypto-1.1.1.tgz#619b70322c9cce4a7ee5fbf8f678b1baa7f06095" integrity sha512-F6tIk8Txdqjg8Siv60iAvXzO9ZdQI87K3sS/fh5xd2XaWK+T5ZfqeTvsT7srwG6fr6uCBfuQEJV1KBBl+JpLZA== +"@elastic/node-crypto@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@elastic/node-crypto/-/node-crypto-1.2.1.tgz#dfd9218f9b5729fa519762e6a6968aaf61b86eb0" + integrity sha512-RlZg+poLA2SwZZUM5RMJDJiKojlSB1mJkumIvLgXvvTCcCliC6rM0lUaNecV9pbQLIHrGlX2BrbwiuPWhv0czQ== + "@elastic/numeral@^2.5.0": version "2.5.0" resolved "https://registry.yarnpkg.com/@elastic/numeral/-/numeral-2.5.0.tgz#8da714827fc278f17546601fdfe55f5c920e2bc5" From e4ddfb8076d45fc30672c1954cbc1336c5d5304e Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Thu, 25 Jun 2020 13:02:33 -0600 Subject: [PATCH 55/82] [7.x] [Maps] Remove maps-telemetry saved object as it is no longer in use (#69871) (#69980) --- x-pack/plugins/maps/common/constants.ts | 1 - .../maps_telemetry/collectors/register.ts | 4 +- x-pack/plugins/maps/server/plugin.ts | 3 +- .../maps/server/saved_objects/index.ts | 1 - .../server/saved_objects/maps_telemetry.ts | 46 ------------------- 5 files changed, 2 insertions(+), 53 deletions(-) delete mode 100644 x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 1d795c370dc00..ea722c18e7005 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -25,7 +25,6 @@ export const EMS_TILES_VECTOR_TILE_PATH = 'vector/tile'; export const MAP_SAVED_OBJECT_TYPE = 'map'; export const APP_ID = 'maps'; export const APP_ICON = 'gisApp'; -export const TELEMETRY_TYPE = APP_ID; export const MAP_APP_PATH = `app/${APP_ID}`; export const GIS_API_PATH = `api/${APP_ID}`; diff --git a/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts b/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts index 383d7773663c6..f54776f5ab629 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts @@ -6,8 +6,6 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { getMapsTelemetry } from '../maps_telemetry'; -// @ts-ignore -import { TELEMETRY_TYPE } from '../../../common/constants'; import { MapsConfigType } from '../../../config'; export function registerMapsUsageCollector( @@ -19,7 +17,7 @@ export function registerMapsUsageCollector( } const mapsUsageCollector = usageCollection.makeUsageCollector({ - type: TELEMETRY_TYPE, + type: 'maps', isReady: () => true, fetch: async () => await getMapsTelemetry(config), }); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index f2331b9a1a960..fe2b73df7978f 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -15,7 +15,7 @@ import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js'; import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js'; import { registerMapsUsageCollector } from './maps_telemetry/collectors/register'; import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, createMapPath } from '../common/constants'; -import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; +import { mapSavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore import { setInternalRepository } from './kibana_server_services'; @@ -191,7 +191,6 @@ export class MapsPlugin implements Plugin { }, }); - core.savedObjects.registerType(mapsTelemetrySavedObjects); core.savedObjects.registerType(mapSavedObjects); registerMapsUsageCollector(usageCollection, currentConfig); diff --git a/x-pack/plugins/maps/server/saved_objects/index.ts b/x-pack/plugins/maps/server/saved_objects/index.ts index c4b779183a2de..804d720a13ab0 100644 --- a/x-pack/plugins/maps/server/saved_objects/index.ts +++ b/x-pack/plugins/maps/server/saved_objects/index.ts @@ -3,5 +3,4 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export { mapsTelemetrySavedObjects } from './maps_telemetry'; export { mapSavedObjects } from './map'; diff --git a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts deleted file mode 100644 index ad0b17af36dda..0000000000000 --- a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { SavedObjectsType } from 'src/core/server'; - -export const mapsTelemetrySavedObjects: SavedObjectsType = { - name: 'maps', - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: { - settings: { - properties: { - showMapVisualizationTypes: { type: 'boolean' }, - }, - }, - indexPatternsWithGeoFieldCount: { type: 'long' }, - indexPatternsWithGeoPointFieldCount: { type: 'long' }, - indexPatternsWithGeoShapeFieldCount: { type: 'long' }, - mapsTotalCount: { type: 'long' }, - timeCaptured: { type: 'date' }, - attributesPerMap: { - properties: { - dataSourcesCount: { - properties: { - min: { type: 'long' }, - max: { type: 'long' }, - avg: { type: 'long' }, - }, - }, - layersCount: { - properties: { - min: { type: 'long' }, - max: { type: 'long' }, - avg: { type: 'long' }, - }, - }, - layerTypesCount: { dynamic: 'true', properties: {} }, - emsVectorLayersCount: { dynamic: 'true', properties: {} }, - }, - }, - }, - }, -}; From 75489919360ca0907a31aeae6a10d11c5c621fb2 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Thu, 25 Jun 2020 14:14:31 -0500 Subject: [PATCH 56/82] [7.x] Index pattern serialize and de-serialize (#68844) (#69979) * Index pattern serialize and de-serialize (#68844) --- ...a-plugin-plugins-data-public.ifieldtype.md | 1 + ...n-plugins-data-public.ifieldtype.tospec.md | 11 + ...plugins-data-public.indexpattern.fields.md | 4 +- ...s-data-public.indexpattern.initfromspec.md | 22 + ...plugin-plugins-data-public.indexpattern.md | 5 +- ...lugins-data-public.indexpattern.tospec.md} | 10 +- ...-public.indexpatternfield._constructor_.md | 4 +- ....indexpatternfield.conflictdescriptions.md | 2 +- ...n-plugins-data-public.indexpatternfield.md | 3 +- ...ns-data-public.indexpatternfield.tospec.md | 11 + ...a-plugin-plugins-data-server.ifieldtype.md | 1 + ...n-plugins-data-server.ifieldtype.tospec.md | 11 + .../stubbed_saved_object_index_pattern.js | 1 + .../fields/__snapshots__/field.test.ts.snap | 40 ++ .../index_patterns/fields/field.test.ts | 24 +- .../common/index_patterns/fields/field.ts | 37 +- .../index_patterns/fields/field_list.ts | 8 +- .../common/index_patterns/fields/types.ts | 6 +- .../__snapshots__/index_pattern.test.ts.snap | 503 ++++++++++++++++++ .../index_patterns/index_patterns/index.ts | 1 - .../index_patterns/index_pattern.test.ts | 27 + .../index_patterns/index_pattern.ts | 65 ++- .../index_patterns/index_patterns.ts | 27 +- .../index_patterns/index_patterns/types.ts | 35 -- .../data/common/index_patterns/types.ts | 65 +++ src/plugins/data/public/index.ts | 5 +- .../data/public/index_patterns/index.ts | 9 +- src/plugins/data/public/public.api.md | 55 +- src/plugins/data/server/server.api.md | 4 + .../sidebar/discover_field.test.tsx | 2 + 30 files changed, 889 insertions(+), 110 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.indexpattern.type.md => kibana-plugin-plugins-data-public.indexpattern.tospec.md} (53%) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md create mode 100644 src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap delete mode 100644 src/plugins/data/common/index_patterns/index_patterns/types.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md index be6af335f20cd..6f42fb32fdb7b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.md @@ -28,6 +28,7 @@ export interface IFieldType | [searchable](./kibana-plugin-plugins-data-public.ifieldtype.searchable.md) | boolean | | | [sortable](./kibana-plugin-plugins-data-public.ifieldtype.sortable.md) | boolean | | | [subType](./kibana-plugin-plugins-data-public.ifieldtype.subtype.md) | IFieldSubType | | +| [toSpec](./kibana-plugin-plugins-data-public.ifieldtype.tospec.md) | () => FieldSpec | | | [type](./kibana-plugin-plugins-data-public.ifieldtype.type.md) | string | | | [visualizable](./kibana-plugin-plugins-data-public.ifieldtype.visualizable.md) | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md new file mode 100644 index 0000000000000..1fb4084c25d34 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ifieldtype.tospec.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IFieldType](./kibana-plugin-plugins-data-public.ifieldtype.md) > [toSpec](./kibana-plugin-plugins-data-public.ifieldtype.tospec.md) + +## IFieldType.toSpec property + +Signature: + +```typescript +toSpec?: () => FieldSpec; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md index 9a93148e4a466..d4dca48c7cd7b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md @@ -7,5 +7,7 @@ Signature: ```typescript -fields: IIndexPatternFieldList; +fields: IIndexPatternFieldList & { + toSpec: () => FieldSpec[]; + }; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md new file mode 100644 index 0000000000000..764dd11638221 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [initFromSpec](./kibana-plugin-plugins-data-public.indexpattern.initfromspec.md) + +## IndexPattern.initFromSpec() method + +Signature: + +```typescript +initFromSpec(spec: IndexPatternSpec): this; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | + +Returns: + +`this` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index ee6da4d91bc13..2dfed6e017afa 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -21,7 +21,7 @@ export declare class IndexPattern implements IIndexPattern | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md) | | any | | -| [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList | | +| [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => FieldSpec[];
} | | | [fieldsFetcher](./kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md) | | any | | | [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | any | | | [formatField](./kibana-plugin-plugins-data-public.indexpattern.formatfield.md) | | any | | @@ -31,7 +31,6 @@ export declare class IndexPattern implements IIndexPattern | [metaFields](./kibana-plugin-plugins-data-public.indexpattern.metafields.md) | | string[] | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpattern.timefieldname.md) | | string | undefined | | | [title](./kibana-plugin-plugins-data-public.indexpattern.title.md) | | string | | -| [type](./kibana-plugin-plugins-data-public.indexpattern.type.md) | | string | | | [typeMeta](./kibana-plugin-plugins-data-public.indexpattern.typemeta.md) | | TypeMeta | | ## Methods @@ -51,6 +50,7 @@ export declare class IndexPattern implements IIndexPattern | [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | | | [getTimeField()](./kibana-plugin-plugins-data-public.indexpattern.gettimefield.md) | | | | [init(forceFieldRefresh)](./kibana-plugin-plugins-data-public.indexpattern.init.md) | | | +| [initFromSpec(spec)](./kibana-plugin-plugins-data-public.indexpattern.initfromspec.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-public.indexpattern.istimebased.md) | | | | [isTimeBasedWildcard()](./kibana-plugin-plugins-data-public.indexpattern.istimebasedwildcard.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-public.indexpattern.istimenanosbased.md) | | | @@ -63,5 +63,6 @@ export declare class IndexPattern implements IIndexPattern | [removeScriptedField(field)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | | | [save(saveAttempts)](./kibana-plugin-plugins-data-public.indexpattern.save.md) | | | | [toJSON()](./kibana-plugin-plugins-data-public.indexpattern.tojson.md) | | | +| [toSpec()](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) | | | | [toString()](./kibana-plugin-plugins-data-public.indexpattern.tostring.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tospec.md similarity index 53% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tospec.md index 58047d9e27ac6..d1a78eea660ce 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.type.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.tospec.md @@ -1,11 +1,15 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [type](./kibana-plugin-plugins-data-public.indexpattern.type.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [toSpec](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) -## IndexPattern.type property +## IndexPattern.toSpec() method Signature: ```typescript -type?: string; +toSpec(): IndexPatternSpec; ``` +Returns: + +`IndexPatternSpec` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md index e1e0d58ce38c1..7a195702b6f13 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `Field` class Signature: ```typescript -constructor(indexPattern: IIndexPattern, spec: FieldSpec | Field, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies); +constructor(indexPattern: IIndexPattern, spec: FieldSpecExportFmt | FieldSpec | Field, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies); ``` ## Parameters @@ -17,7 +17,7 @@ constructor(indexPattern: IIndexPattern, spec: FieldSpec | Field, shortDotsEnabl | Parameter | Type | Description | | --- | --- | --- | | indexPattern | IIndexPattern | | -| spec | FieldSpec | Field | | +| spec | FieldSpecExportFmt | FieldSpec | Field | | | shortDotsEnable | boolean | | | { fieldFormats, onNotification } | FieldDependencies | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md index ca2552aeb1b42..ec19a4854bf0e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md @@ -7,5 +7,5 @@ Signature: ```typescript -conflictDescriptions?: Record; +conflictDescriptions?: FieldSpecConflictDescriptions; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index 8fa1ee0d72e54..d82999e7a96af 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -22,7 +22,7 @@ export declare class Field implements IFieldType | --- | --- | --- | --- | | [$$spec](./kibana-plugin-plugins-data-public.indexpatternfield.__spec.md) | | FieldSpec | | | [aggregatable](./kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md) | | boolean | | -| [conflictDescriptions](./kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md) | | Record<string, string[]> | | +| [conflictDescriptions](./kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md) | | FieldSpecConflictDescriptions | | | [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) | | number | | | [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | | @@ -37,6 +37,7 @@ export declare class Field implements IFieldType | [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) | | boolean | | | [sortable](./kibana-plugin-plugins-data-public.indexpatternfield.sortable.md) | | boolean | | | [subType](./kibana-plugin-plugins-data-public.indexpatternfield.subtype.md) | | IFieldSubType | | +| [toSpec](./kibana-plugin-plugins-data-public.indexpatternfield.tospec.md) | | () => FieldSpecExportFmt | | | [type](./kibana-plugin-plugins-data-public.indexpatternfield.type.md) | | string | | | [visualizable](./kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md new file mode 100644 index 0000000000000..35714faa03bc9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [toSpec](./kibana-plugin-plugins-data-public.indexpatternfield.tospec.md) + +## IndexPatternField.toSpec property + +Signature: + +```typescript +toSpec: () => FieldSpecExportFmt; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md index 5375cf2a2ef43..77a2954428f8d 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.md @@ -28,6 +28,7 @@ export interface IFieldType | [searchable](./kibana-plugin-plugins-data-server.ifieldtype.searchable.md) | boolean | | | [sortable](./kibana-plugin-plugins-data-server.ifieldtype.sortable.md) | boolean | | | [subType](./kibana-plugin-plugins-data-server.ifieldtype.subtype.md) | IFieldSubType | | +| [toSpec](./kibana-plugin-plugins-data-server.ifieldtype.tospec.md) | () => FieldSpec | | | [type](./kibana-plugin-plugins-data-server.ifieldtype.type.md) | string | | | [visualizable](./kibana-plugin-plugins-data-server.ifieldtype.visualizable.md) | boolean | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md new file mode 100644 index 0000000000000..d1863bebce4f0 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ifieldtype.tospec.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) > [toSpec](./kibana-plugin-plugins-data-server.ifieldtype.tospec.md) + +## IFieldType.toSpec property + +Signature: + +```typescript +toSpec?: () => FieldSpec; +``` diff --git a/src/fixtures/stubbed_saved_object_index_pattern.js b/src/fixtures/stubbed_saved_object_index_pattern.js index 15e47b40eb203..8e0e230ef33dd 100644 --- a/src/fixtures/stubbed_saved_object_index_pattern.js +++ b/src/fixtures/stubbed_saved_object_index_pattern.js @@ -27,6 +27,7 @@ export function stubbedSavedObjectIndexPattern(id) { id, type: 'index-pattern', attributes: { + timeFieldName: 'timestamp', customFormats: '{}', fields: mockLogstashFields, }, diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap index 4593349a408a7..e61593f6bfb27 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/field.test.ts.snap @@ -33,3 +33,43 @@ Object { "type": "type", } `; + +exports[`Field spec snapshot 1`] = ` +Object { + "aggregatable": true, + "conflictDescriptions": Object { + "a": Array [ + "b", + "c", + ], + "d": Array [ + "e", + ], + }, + "count": 1, + "esTypes": Array [ + "type", + ], + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, + "lang": "lang", + "name": "name", + "readFromDocValues": false, + "script": "script", + "scripted": true, + "searchable": true, + "subType": Object { + "multi": Object { + "parent": "parent", + }, + "nested": Object { + "path": "path", + }, + }, + "type": "type", +} +`; diff --git a/src/plugins/data/common/index_patterns/fields/field.test.ts b/src/plugins/data/common/index_patterns/fields/field.test.ts index 711c176fed9cc..910f22088f43a 100644 --- a/src/plugins/data/common/index_patterns/fields/field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/field.test.ts @@ -20,7 +20,7 @@ import { Field } from './field'; import { IndexPattern } from '../index_patterns'; import { FieldFormatsStartCommon } from '../..'; -import { KBN_FIELD_TYPES } from '../../../common'; +import { KBN_FIELD_TYPES, FieldSpec, FieldSpecExportFmt } from '../../../common'; describe('Field', function () { function flatten(obj: Record) { @@ -59,8 +59,9 @@ describe('Field', function () { fieldFormatMap: { name: {}, _source: {}, _score: {}, _id: {} }, } as unknown) as IndexPattern, format: { name: 'formatName' }, - $$spec: {}, + $$spec: ({} as unknown) as FieldSpec, conflictDescriptions: { a: ['b', 'c'], d: ['e'] }, + toSpec: () => (({} as unknown) as FieldSpecExportFmt), } as Field; it('the correct properties are writable', () => { @@ -145,7 +146,7 @@ describe('Field', function () { }).toThrow(); expect(() => { - field.$$spec = { a: 'b' }; + field.$$spec = ({ a: 'b' } as unknown) as FieldSpec; }).toThrow(); }); @@ -219,4 +220,21 @@ describe('Field', function () { }); expect(flatten(field)).toMatchSnapshot(); }); + + it('spec snapshot', () => { + const field = new Field( + { + fieldFormatMap: { + name: { toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }) }, + }, + } as IndexPattern, + fieldValues, + false, + { + fieldFormats: {} as FieldFormatsStartCommon, + onNotification: () => {}, + } + ); + expect(field.toSpec()).toMatchSnapshot(); + }); }); diff --git a/src/plugins/data/common/index_patterns/fields/field.ts b/src/plugins/data/common/index_patterns/fields/field.ts index c53e3f2b1f621..81c7aff8a0faa 100644 --- a/src/plugins/data/common/index_patterns/fields/field.ts +++ b/src/plugins/data/common/index_patterns/fields/field.ts @@ -28,11 +28,14 @@ import { FieldFormat, shortenDottedString, } from '../../../common'; -import { OnNotification } from '../types'; +import { + OnNotification, + FieldSpec, + FieldSpecConflictDescriptions, + FieldSpecExportFmt, +} from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; -export type FieldSpec = Record; - interface FieldDependencies { fieldFormats: FieldFormatsStartCommon; onNotification: OnNotification; @@ -59,11 +62,11 @@ export class Field implements IFieldType { readFromDocValues?: boolean; format: any; $$spec: FieldSpec; - conflictDescriptions?: Record; + conflictDescriptions?: FieldSpecConflictDescriptions; constructor( indexPattern: IIndexPattern, - spec: FieldSpec | Field, + spec: FieldSpecExportFmt | FieldSpec | Field, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies ) { @@ -95,7 +98,7 @@ export class Field implements IFieldType { if (!type) type = getKbnFieldType('unknown'); - let format = spec.format; + let format: any = spec.format; if (!FieldFormat.isInstanceOfFieldFormat(format)) { format = @@ -148,6 +151,26 @@ export class Field implements IFieldType { // multi info obj.fact('subType'); - return obj.create(); + const newObj = obj.create(); + newObj.toSpec = function () { + return { + count: this.count, + script: this.script, + lang: this.lang, + conflictDescriptions: this.conflictDescriptions, + name: this.name, + type: this.type, + esTypes: this.esTypes, + scripted: this.scripted, + searchable: this.searchable, + aggregatable: this.aggregatable, + readFromDocValues: this.readFromDocValues, + subType: this.subType, + format: this.indexPattern?.fieldFormatMap[this.name]?.toJSON() || undefined, + }; + }; + return newObj; } + // only providing type info as constructor returns new object instead of `this` + toSpec = () => (({} as unknown) as FieldSpecExportFmt); } diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts index 173a629863a71..c1ca5341328ce 100644 --- a/src/plugins/data/common/index_patterns/fields/field_list.ts +++ b/src/plugins/data/common/index_patterns/fields/field_list.ts @@ -20,8 +20,8 @@ import { findIndex } from 'lodash'; import { IIndexPattern } from '../../types'; import { IFieldType } from '../../../common'; -import { Field, FieldSpec } from './field'; -import { OnNotification } from '../types'; +import { Field } from './field'; +import { OnNotification, FieldSpec } from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; type FieldMap = Map; @@ -102,6 +102,10 @@ export const getIndexPatternFieldListCreator = ({ this.removeByGroup(newField); this.setByGroup(newField); }; + + toSpec = () => { + return [...this.map((field) => field.toSpec())]; + }; } return new FieldList(...fieldListParams); diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index c336472a1e7d6..558b5b57dce40 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -17,10 +17,7 @@ * under the License. */ -export interface IFieldSubType { - multi?: { parent: string }; - nested?: { path: string }; -} +import { FieldSpec, IFieldSubType } from '../types'; export interface IFieldType { name: string; @@ -41,4 +38,5 @@ export interface IFieldType { subType?: IFieldSubType; displayName?: string; format?: any; + toSpec?: () => FieldSpec; } diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap new file mode 100644 index 0000000000000..047ac836a87d1 --- /dev/null +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -0,0 +1,503 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IndexPattern toSpec should match snapshot 1`] = ` +Object { + "fields": Array [ + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 10, + "esTypes": Array [ + "long", + ], + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, + "lang": undefined, + "name": "bytes", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "number", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 20, + "esTypes": Array [ + "boolean", + ], + "format": undefined, + "lang": undefined, + "name": "ssl", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "boolean", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 30, + "esTypes": Array [ + "date", + ], + "format": undefined, + "lang": undefined, + "name": "@timestamp", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "date", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 30, + "esTypes": Array [ + "date", + ], + "format": undefined, + "lang": undefined, + "name": "time", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "date", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "keyword", + ], + "format": undefined, + "lang": undefined, + "name": "@tags", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "date", + ], + "format": undefined, + "lang": undefined, + "name": "utc_time", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "date", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "integer", + ], + "format": undefined, + "lang": undefined, + "name": "phpmemory", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "number", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "ip", + ], + "format": undefined, + "lang": undefined, + "name": "ip", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "ip", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "attachment", + ], + "format": undefined, + "lang": undefined, + "name": "request_body", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "attachment", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "geo_point", + ], + "format": undefined, + "lang": undefined, + "name": "point", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "geo_point", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "geo_shape", + ], + "format": undefined, + "lang": undefined, + "name": "area", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "geo_shape", + }, + Object { + "aggregatable": false, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "murmur3", + ], + "format": undefined, + "lang": undefined, + "name": "hashed", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "murmur3", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "geo_point", + ], + "format": undefined, + "lang": undefined, + "name": "geo.coordinates", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "geo_point", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "text", + ], + "format": undefined, + "lang": undefined, + "name": "extension", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "keyword", + ], + "format": undefined, + "lang": undefined, + "name": "extension.keyword", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": Object { + "multi": Object { + "parent": "extension", + }, + }, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "text", + ], + "format": undefined, + "lang": undefined, + "name": "machine.os", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "keyword", + ], + "format": undefined, + "lang": undefined, + "name": "machine.os.raw", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": Object { + "multi": Object { + "parent": "machine.os", + }, + }, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "keyword", + ], + "format": undefined, + "lang": undefined, + "name": "geo.src", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "_id", + ], + "format": undefined, + "lang": undefined, + "name": "_id", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "_type", + ], + "format": undefined, + "lang": undefined, + "name": "_type", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "_source", + ], + "format": undefined, + "lang": undefined, + "name": "_source", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "_source", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "text", + ], + "format": undefined, + "lang": undefined, + "name": "non-filterable", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": false, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": false, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "text", + ], + "format": undefined, + "lang": undefined, + "name": "non-sortable", + "readFromDocValues": false, + "script": undefined, + "scripted": false, + "searchable": false, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "conflict", + ], + "format": undefined, + "lang": undefined, + "name": "custom_user_field", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": undefined, + "type": "conflict", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "text", + ], + "format": undefined, + "lang": "expression", + "name": "script string", + "readFromDocValues": false, + "script": "'i am a string'", + "scripted": true, + "searchable": true, + "subType": undefined, + "type": "string", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "long", + ], + "format": undefined, + "lang": "expression", + "name": "script number", + "readFromDocValues": false, + "script": "1234", + "scripted": true, + "searchable": true, + "subType": undefined, + "type": "number", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "date", + ], + "format": undefined, + "lang": "painless", + "name": "script date", + "readFromDocValues": false, + "script": "1234", + "scripted": true, + "searchable": true, + "subType": undefined, + "type": "date", + }, + Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "murmur3", + ], + "format": undefined, + "lang": "expression", + "name": "script murmur3", + "readFromDocValues": false, + "script": "1234", + "scripted": true, + "searchable": true, + "subType": undefined, + "type": "murmur3", + }, + ], + "id": "test-pattern", + "sourceFilters": undefined, + "timeFieldName": "timestamp", + "title": "test-pattern", + "typeMeta": undefined, + "version": 2, +} +`; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index_patterns/index.ts index 5fae08f3bb775..77527857ed0ca 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index.ts @@ -18,7 +18,6 @@ */ export * from './index_patterns_api_client'; -export * from './types'; export * from './_pattern_cache'; export * from './flatten_hit'; export * from './format_hit'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index 6d7c5ce5e572a..2d6f745c330e1 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -30,6 +30,10 @@ import { Field } from '../fields'; import { fieldFormatsMock } from '../../field_formats/mocks'; +class MockFieldFormatter {} + +fieldFormatsMock.getType = jest.fn().mockImplementation(() => MockFieldFormatter); + jest.mock('../../field_mapping', () => { const originalModule = jest.requireActual('../../field_mapping'); @@ -304,6 +308,29 @@ describe('IndexPattern', () => { }); }); + describe('toSpec', () => { + test('should match snapshot', () => { + indexPattern.fieldFormatMap.bytes = { + toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }), + }; + expect(indexPattern.toSpec()).toMatchSnapshot(); + }); + + test('can restore from spec', async () => { + indexPattern.fieldFormatMap.bytes = { + toJSON: () => ({ id: 'number', params: { pattern: '$0,0.[00]' } }), + }; + const spec = indexPattern.toSpec(); + const restoredPattern = await create(spec.id as string); + restoredPattern.initFromSpec(spec); + expect(restoredPattern.id).toEqual(indexPattern.id); + expect(restoredPattern.title).toEqual(indexPattern.title); + expect(restoredPattern.timeFieldName).toEqual(indexPattern.timeFieldName); + expect(restoredPattern.fields.length).toEqual(indexPattern.fields.length); + expect(restoredPattern.fieldFormatMap.bytes instanceof MockFieldFormatter).toEqual(true); + }); + }); + describe('popularizeField', () => { test('should increment the popularity count by default', () => { // const saveSpy = sinon.stub(indexPattern, 'save'); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 28797533bf567..013899a0c14b4 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -21,6 +21,7 @@ import _, { each, reject } from 'lodash'; import { i18n } from '@kbn/i18n'; import { SavedObjectsClientContract } from 'src/core/public'; +import { SavedObjectAttributes } from 'src/core/public'; import { DuplicateField, SavedObjectNotFound } from '../../../../kibana_utils/common'; import { @@ -37,11 +38,12 @@ import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; import { IIndexPatternsApiClient } from '.'; -import { TypeMeta } from '.'; import { OnNotification, OnError, OnUnsupportedTimePattern } from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; import { PatternCache } from './_pattern_cache'; import { expandShorthand, FieldMappingSpec, MappingObject } from '../../field_mapping'; +import { IndexPatternSpec, TypeMeta, FieldSpec, SourceFilter } from '../types'; +import { SerializedFieldFormat } from '../../../../expressions/common'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; const type = 'index-pattern'; @@ -62,10 +64,9 @@ export class IndexPattern implements IIndexPattern { public id?: string; public title: string = ''; - public type?: string; public fieldFormatMap: any; public typeMeta?: TypeMeta; - public fields: IIndexPatternFieldList; + public fields: IIndexPatternFieldList & { toSpec: () => FieldSpec[] }; public timeFieldName: string | undefined; public intervalName: string | undefined | null; public formatHit: any; @@ -77,7 +78,7 @@ export class IndexPattern implements IIndexPattern { private savedObjectsClient: SavedObjectsClientContract; private patternCache: PatternCache; private getConfig: any; - private sourceFilters?: []; + private sourceFilters?: SourceFilter[]; private originalBody: { [key: string]: any } = {}; public fieldsFetcher: any; // probably want to factor out any direct usage and change to private private shortDotsEnable: boolean = false; @@ -202,6 +203,35 @@ export class IndexPattern implements IIndexPattern { this.initFields(); } + public initFromSpec(spec: IndexPatternSpec) { + // create fieldFormatMap from field list + const fieldFormatMap: Record = {}; + if (_.isArray(spec.fields)) { + spec.fields.forEach((field: FieldSpec) => { + if (field.format) { + fieldFormatMap[field.name as string] = { ...field.format }; + } + }); + } + + this.version = spec.version; + + this.title = spec.title || ''; + this.timeFieldName = spec.timeFieldName; + this.sourceFilters = spec.sourceFilters; + + // ignoring this because the same thing happens elsewhere but via _.assign + // @ts-ignore + this.fields = spec.fields || []; + this.typeMeta = spec.typeMeta; + this.fieldFormatMap = _.mapValues(fieldFormatMap, (mapping) => { + return this.deserializeFieldFormatMap(mapping); + }); + + this.initFields(); + return this; + } + private async updateFromElasticSearch(response: any, forceFieldRefresh: boolean = false) { if (!response.found) { throw new SavedObjectNotFound(type, this.id, 'management/kibana/indexPatterns'); @@ -212,15 +242,16 @@ export class IndexPattern implements IIndexPattern { return; } - response._source[name] = fieldMapping._deserialize(response._source[name]); + response[name] = fieldMapping._deserialize(response[name]); }); - // give index pattern all of the values in _source - _.assign(this, response._source); + // give index pattern all of the values + _.assign(this, response); if (!this.title && this.id) { this.title = this.id; } + this.version = response.version; if (this.isUnsupportedTimePattern()) { this.onUnsupportedTimePattern({ @@ -280,13 +311,11 @@ export class IndexPattern implements IIndexPattern { } const savedObject = await this.savedObjectsClient.get(type, this.id); - this.version = savedObject._version; const response = { - _id: savedObject.id, - _type: savedObject.type, - _source: _.cloneDeep(savedObject.attributes), + version: savedObject._version, found: savedObject._version ? true : false, + ...(_.cloneDeep(savedObject.attributes) as SavedObjectAttributes), }; // Do this before we attempt to update from ES since that call can potentially perform a save this.originalBody = this.prepBody(); @@ -297,6 +326,7 @@ export class IndexPattern implements IIndexPattern { return this; } + // todo - is this needed? migrate(newTitle: string) { return this.savedObjectsClient .update(type, this.id!, { @@ -310,6 +340,19 @@ export class IndexPattern implements IIndexPattern { .then(() => this); } + public toSpec(): IndexPatternSpec { + return { + id: this.id, + version: this.version, + + title: this.title, + timeFieldName: this.timeFieldName, + sourceFilters: this.sourceFilters, + fields: this.fields.toSpec(), + typeMeta: this.typeMeta, + }; + } + // Get the source filtering configuration for that index. getSourceFiltering() { return { diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index e501364e10439..a16625fe33122 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -32,13 +32,14 @@ import { createEnsureDefaultIndexPattern, EnsureDefaultIndexPattern, } from './ensure_default_index_pattern'; +import { getIndexPatternFieldListCreator, CreateIndexPatternFieldList, Field } from '../fields'; import { - getIndexPatternFieldListCreator, - CreateIndexPatternFieldList, - Field, + OnNotification, + OnError, + OnUnsupportedTimePattern, + IndexPatternSpec, FieldSpec, -} from '../fields'; -import { OnNotification, OnError, OnUnsupportedTimePattern } from '../types'; +} from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; const indexPatternCache = createIndexPatternCache(); @@ -199,6 +200,22 @@ export class IndexPatternsService { return indexPatternCache.set(id, indexPattern); }; + specToIndexPattern(spec: IndexPatternSpec) { + const indexPattern = new IndexPattern(spec.id, { + getConfig: (cfg: any) => this.config.get(cfg), + savedObjectsClient: this.savedObjectsClient, + apiClient: this.apiClient, + patternCache: indexPatternCache, + fieldFormats: this.fieldFormats, + onNotification: this.onNotification, + onError: this.onError, + onUnsupportedTimePattern: this.onUnsupportedTimePattern, + }); + + indexPattern.initFromSpec(spec); + return indexPattern; + } + make = (id?: string): Promise => { const indexPattern = new IndexPattern(id, { getConfig: (cfg: any) => this.config.get(cfg), diff --git a/src/plugins/data/common/index_patterns/index_patterns/types.ts b/src/plugins/data/common/index_patterns/index_patterns/types.ts deleted file mode 100644 index b2060dd1d48ba..0000000000000 --- a/src/plugins/data/common/index_patterns/index_patterns/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export type AggregationRestrictions = Record< - string, - { - agg?: string; - interval?: number; - fixed_interval?: string; - calendar_interval?: string; - delay?: string; - time_zone?: string; - } ->; - -export interface TypeMeta { - aggs?: Record; - [key: string]: any; -} diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 1849db6310843..58d90e13fe636 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -19,6 +19,8 @@ import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications'; import { IFieldType } from './fields'; +import { SerializedFieldFormat } from '../../../expressions/common'; +import { KBN_FIELD_TYPES } from '..'; export interface IIndexPattern { [key: string]: any; @@ -52,6 +54,7 @@ export interface IndexPatternAttributes { export type OnNotification = (toastInputFields: ToastInputFields) => void; export type OnError = (error: Error, toastInputFields: ErrorToastOptions) => void; + export type OnUnsupportedTimePattern = ({ id, title, @@ -61,3 +64,65 @@ export type OnUnsupportedTimePattern = ({ title: string; index: string; }) => void; + +export type AggregationRestrictions = Record< + string, + { + agg?: string; + interval?: number; + fixed_interval?: string; + calendar_interval?: string; + delay?: string; + time_zone?: string; + } +>; + +export interface IFieldSubType { + multi?: { parent: string }; + nested?: { path: string }; +} + +export interface TypeMeta { + aggs?: Record; + [key: string]: any; +} + +export type FieldSpecConflictDescriptions = Record; + +// This should become FieldSpec once types are cleaned up +export interface FieldSpecExportFmt { + count?: number; + script?: string; + lang?: string; + conflictDescriptions?: FieldSpecConflictDescriptions; + name: string; + type: KBN_FIELD_TYPES; + esTypes?: string[]; + scripted: boolean; + searchable: boolean; + aggregatable: boolean; + readFromDocValues?: boolean; + subType?: IFieldSubType; + format?: SerializedFieldFormat; + indexed?: boolean; +} + +export interface FieldSpec { + [key: string]: any; + format?: SerializedFieldFormat; +} + +export interface IndexPatternSpec { + id?: string; + version?: string; + + title: string; + timeFieldName?: string; + sourceFilters?: SourceFilter[]; + fields?: FieldSpec[]; + typeMeta?: TypeMeta; +} + +export interface SourceFilter { + value: string; +} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index d3552cf72a27e..b0ceb6e642ee0 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -249,9 +249,6 @@ export { IndexPattern, IIndexPatternFieldList, Field as IndexPatternField, - TypeMeta as IndexPatternTypeMeta, - AggregationRestrictions as IndexPatternAggRestrictions, - // TODO: exported only in stub_index_pattern test. Move into data plugin and remove export getIndexPatternFieldListCreator, } from './index_patterns'; @@ -263,6 +260,8 @@ export { KBN_FIELD_TYPES, IndexPatternAttributes, UI_SETTINGS, + TypeMeta as IndexPatternTypeMeta, + AggregationRestrictions as IndexPatternAggRestrictions, } from '../common'; /* diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts index 86d1e632b267d..c3b1d4b63728f 100644 --- a/src/plugins/data/public/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index.ts @@ -39,11 +39,4 @@ export { IIndexPatternFieldList, } from '../../common/index_patterns'; -// TODO: figure out how to replace IndexPatterns in get_inner_angular. -export { - IndexPatternsService, - IndexPatternsContract, - IndexPattern, - TypeMeta, - AggregationRestrictions, -} from './index_patterns'; +export { IndexPatternsService, IndexPatternsContract, IndexPattern } from './index_patterns'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 6d6bb09ccd7dc..aa9fd99e6e5ae 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -902,6 +902,10 @@ export interface IFieldType { sortable?: boolean; // (undocumented) subType?: IFieldSubType; + // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) + toSpec?: () => FieldSpec; // (undocumented) type: string; // (undocumented) @@ -939,8 +943,6 @@ export interface IIndexPattern { // // @public (undocumented) export interface IIndexPatternFieldList extends Array { - // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts - // // (undocumented) add(field: FieldSpec): void; // (undocumented) @@ -995,7 +997,9 @@ export class IndexPattern implements IIndexPattern { // (undocumented) fieldFormatMap: any; // (undocumented) - fields: IIndexPatternFieldList; + fields: IIndexPatternFieldList & { + toSpec: () => FieldSpec[]; + }; // (undocumented) fieldsFetcher: any; // (undocumented) @@ -1040,6 +1044,10 @@ export class IndexPattern implements IIndexPattern { id?: string; // (undocumented) init(forceFieldRefresh?: boolean): Promise; + // Warning: (ae-forgotten-export) The symbol "IndexPatternSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) + initFromSpec(spec: IndexPatternSpec): this; // (undocumented) intervalName: string | undefined | null; // (undocumented) @@ -1075,9 +1083,9 @@ export class IndexPattern implements IIndexPattern { // (undocumented) toJSON(): string | undefined; // (undocumented) - toString(): string; + toSpec(): IndexPatternSpec; // (undocumented) - type?: string; + toString(): string; // (undocumented) typeMeta?: IndexPatternTypeMeta; } @@ -1116,12 +1124,15 @@ export interface IndexPatternAttributes { export class IndexPatternField implements IFieldType { // (undocumented) $$spec: FieldSpec; + // Warning: (ae-forgotten-export) The symbol "FieldSpecExportFmt" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FieldDependencies" needs to be exported by the entry point index.d.ts - constructor(indexPattern: IIndexPattern, spec: FieldSpec | IndexPatternField, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies); + constructor(indexPattern: IIndexPattern, spec: FieldSpecExportFmt | FieldSpec | IndexPatternField, shortDotsEnable: boolean, { fieldFormats, onNotification }: FieldDependencies); // (undocumented) aggregatable?: boolean; + // Warning: (ae-forgotten-export) The symbol "FieldSpecConflictDescriptions" needs to be exported by the entry point index.d.ts + // // (undocumented) - conflictDescriptions?: Record; + conflictDescriptions?: FieldSpecConflictDescriptions; // (undocumented) count?: number; // (undocumented) @@ -1151,6 +1162,8 @@ export class IndexPatternField implements IFieldType { // (undocumented) subType?: IFieldSubType; // (undocumented) + toSpec: () => FieldSpecExportFmt; + // (undocumented) type: string; // (undocumented) visualizable?: boolean; @@ -1985,20 +1998,20 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:233:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:373:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:373:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:373:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:373:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:375:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:376:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:372:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:372:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:372:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:372:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:374:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:375:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:384:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:40:60 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts // src/plugins/data/public/types.ts:53:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 829045bb167be..f2a40fd48d7c1 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -392,6 +392,10 @@ export interface IFieldType { sortable?: boolean; // (undocumented) subType?: IFieldSubType; + // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) + toSpec?: () => FieldSpec; // (undocumented) type: string; // (undocumented) diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 8c527475b7480..099ec2e5b1ffc 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -28,6 +28,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { DiscoverField } from './discover_field'; import { coreMock } from '../../../../../../core/public/mocks'; import { IndexPatternField } from '../../../../../data/public'; +import { FieldSpecExportFmt } from '../../../../../data/common'; jest.mock('../../../kibana_services', () => ({ getServices: () => ({ @@ -74,6 +75,7 @@ function getComponent(selected = false, showDetails = false, useShortDots = fals format: null, routes: {}, $$spec: {}, + toSpec: () => (({} as unknown) as FieldSpecExportFmt), } as IndexPatternField; const props = { From b03ab11025fccea32d74027547192f658d0bbe8c Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Thu, 25 Jun 2020 21:16:04 +0200 Subject: [PATCH 57/82] Fixes #69639: Ignore url.url fields above 2048 characters (#69863) (#69930) --- src/plugins/share/server/saved_objects/url.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/share/server/saved_objects/url.ts b/src/plugins/share/server/saved_objects/url.ts index c76c21993a13f..3ea64ad4719f7 100644 --- a/src/plugins/share/server/saved_objects/url.ts +++ b/src/plugins/share/server/saved_objects/url.ts @@ -46,6 +46,7 @@ export const url: SavedObjectsType = { fields: { keyword: { type: 'keyword', + ignore_above: 2048, }, }, }, From 49985ede648fed5f1396f92f8baefb63d9eeea17 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Thu, 25 Jun 2020 15:32:54 -0400 Subject: [PATCH 58/82] rename old siem kibana config to securitySolution (#69874) (#69989) Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../plugins/security_solution/server/index.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/index.ts b/x-pack/plugins/security_solution/server/index.ts index 8a77137c20c11..06b35213b4713 100644 --- a/x-pack/plugins/security_solution/server/index.ts +++ b/x-pack/plugins/security_solution/server/index.ts @@ -4,15 +4,41 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; import { Plugin, PluginSetup, PluginStart } from './plugin'; import { configSchema, ConfigType } from './config'; +import { SIGNALS_INDEX_KEY } from '../common/constants'; export const plugin = (context: PluginInitializerContext) => { return new Plugin(context); }; -export const config = { schema: configSchema }; +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('xpack.siem.enabled', 'xpack.securitySolution.enabled'), + renameFromRoot( + 'xpack.siem.maxRuleImportExportSize', + 'xpack.securitySolution.maxRuleImportExportSize' + ), + renameFromRoot( + 'xpack.siem.maxRuleImportPayloadBytes', + 'xpack.securitySolution.maxRuleImportPayloadBytes' + ), + renameFromRoot( + 'xpack.siem.maxTimelineImportExportSize', + 'xpack.securitySolution.maxTimelineImportExportSize' + ), + renameFromRoot( + 'xpack.siem.maxTimelineImportPayloadBytes', + 'xpack.securitySolution.maxTimelineImportPayloadBytes' + ), + renameFromRoot( + `xpack.siem.${SIGNALS_INDEX_KEY}`, + `xpack.securitySolution.${SIGNALS_INDEX_KEY}` + ), + ], +}; export { ConfigType, Plugin, PluginSetup, PluginStart }; From 7502a6620f86c8e64503faa152373c65f94ed4bf Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 25 Jun 2020 13:53:10 -0600 Subject: [PATCH 59/82] [Security Solution] [Timeline] fix bug for filter manager #69870 (#69986) --- .../draggable_wrapper_hover_content.test.tsx | 80 ++++++++++++++++--- .../draggable_wrapper_hover_content.tsx | 20 +++-- 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index 16207fcec3b26..ee1dc73b27fe2 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -20,6 +20,7 @@ import { ManageGlobalTimeline, timelineDefaults, } from '../../../timelines/components/manage_timeline'; +import { TimelineId } from '../../../../common/types/timeline'; jest.mock('../link_to'); @@ -41,9 +42,24 @@ jest.mock('uuid', () => { }); jest.mock('../../hooks/use_add_to_timeline'); +const mockAddFilters = jest.fn(); +const mockGetTimelineFilterManager = jest.fn().mockReturnValue({ + addFilters: mockAddFilters, +}); +jest.mock('../../../timelines/components/manage_timeline', () => { + const original = jest.requireActual('../../../timelines/components/manage_timeline'); + + return { + ...original, + useManageTimeline: () => ({ + getTimelineFilterManager: mockGetTimelineFilterManager, + isManagedTimeline: jest.fn().mockReturnValue(false), + }), + }; +}); const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; -const timelineId = 'cool-id'; +const timelineId = TimelineId.active; const field = 'process.name'; const value = 'nice'; const toggleTopN = jest.fn(); @@ -88,6 +104,9 @@ describe('DraggableWrapperHoverContent', () => { forOrOut.forEach((hoverAction) => { describe(`Filter ${hoverAction} value`, () => { + beforeEach(() => { + jest.clearAllMocks(); + }); test(`it renders the 'Filter ${hoverAction} value' button when showTopN is false`, () => { const wrapper = mount( @@ -111,21 +130,16 @@ describe('DraggableWrapperHoverContent', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().exists() ).toBe(false); }); - describe('when run in the context of a timeline', () => { - let filterManager: FilterManager; let wrapper: ReactWrapper; let onFilterAdded: () => void; beforeEach(() => { - filterManager = new FilterManager(mockUiSettingsForFilterManager); - filterManager.addFilters = jest.fn(); onFilterAdded = jest.fn(); const manageTimelineForTesting = { [timelineId]: { ...timelineDefaults, id: timelineId, - filterManager, }, }; @@ -141,7 +155,7 @@ describe('DraggableWrapperHoverContent', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); wrapper.update(); - expect(filterManager.addFilters).toBeCalledWith({ + expect(mockAddFilters).toBeCalledWith({ meta: { alias: null, disabled: false, @@ -174,7 +188,9 @@ describe('DraggableWrapperHoverContent', () => { wrapper = mount( - + ); }); @@ -263,7 +279,7 @@ describe('DraggableWrapperHoverContent', () => { wrapper.find(`[data-test-subj="filter-${hoverAction}-value"]`).first().simulate('click'); wrapper.update(); - expect(filterManager.addFilters).toBeCalledWith(expected); + expect(mockAddFilters).toBeCalledWith(expected); }); }); @@ -278,7 +294,14 @@ describe('DraggableWrapperHoverContent', () => { wrapper = mount( - + ); }); @@ -544,4 +567,41 @@ describe('DraggableWrapperHoverContent', () => { expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(false); }); }); + + describe('Filter Manager', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + test('filter manager, not active timeline', () => { + mount( + + + + ); + + expect(mockGetTimelineFilterManager).not.toBeCalled(); + }); + test('filter manager, active timeline', () => { + mount( + + + + ); + + expect(mockGetTimelineFilterManager).toBeCalled(); + }); + test('filter manager, active timeline in draggableId', () => { + mount( + + + + ); + + expect(mockGetTimelineFilterManager).toBeCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index e805750cf2477..4efdea5eee43b 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -13,11 +13,12 @@ import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { useKibana } from '../../lib/kibana'; import { createFilter } from '../add_filter_to_global_search_bar'; -import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '../top_n'; +import { StatefulTopN } from '../top_n'; import { allowTopN } from './helpers'; import * as i18n from './translations'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { TimelineId } from '../../../../common/types/timeline'; interface Props { draggableId?: DraggableId; @@ -34,7 +35,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ field, onFilterAdded, showTopN, - timelineId = ACTIVE_TIMELINE_REDUX_ID, + timelineId, toggleTopN, value, }) => { @@ -44,11 +45,16 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ kibana.services.data.query.filterManager, ]); const { getTimelineFilterManager } = useManageTimeline(); - const filterManager = useMemo(() => getTimelineFilterManager(timelineId) ?? filterManagerBackup, [ - timelineId, - getTimelineFilterManager, - filterManagerBackup, - ]); + + const filterManager = useMemo( + () => + timelineId === TimelineId.active || + (draggableId != null && draggableId?.includes(TimelineId.active)) + ? getTimelineFilterManager(TimelineId.active) + : filterManagerBackup, + [draggableId, timelineId, getTimelineFilterManager, filterManagerBackup] + ); + const filterForValue = useCallback(() => { const filter = value?.length === 0 ? createFilter(field, undefined) : createFilter(field, value); From 3eebf98e664b355bb30bbf85f081a1ad51acf5c1 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Thu, 25 Jun 2020 13:11:58 -0700 Subject: [PATCH 60/82] Search profiler functional test -- using "test_user" with limited role. (#69841) (#69993) * using test_user with limited read permission to search profiler test * gitcheck * search profiler test using test_user --- .../apps/dev_tools/searchprofiler_editor.ts | 6 ++++++ x-pack/test/functional/config.js | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts index 3483ddf769e5f..bf2a4192af543 100644 --- a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts +++ b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts @@ -12,15 +12,21 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const aceEditor = getService('aceEditor'); const retry = getService('retry'); + const security = getService('security'); const editorTestSubjectSelector = 'searchProfilerEditor'; describe('Search Profiler Editor', () => { before(async () => { + await security.testUser.setRoles(['global_devtools_read']); await PageObjects.common.navigateToApp('searchProfiler'); expect(await testSubjects.exists('searchProfilerEditor')).to.be(true); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('correctly parses triple quotes in JSON', async () => { // The below inputs are written to work _with_ ace's autocomplete unlike console's unit test // counterparts in src/legacy/core_plugins/console/public/tests/src/editor.test.js diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 94c90143b5776..aaddc460f89cf 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -231,6 +231,17 @@ export default async function ({ readConfigFile }) { ], }, + global_devtools_read: { + kibana: [ + { + feature: { + dev_tools: ['read'], + }, + spaces: ['*'], + }, + ], + }, + //Kibana feature privilege isn't specific to advancedSetting. It can be anything. https://github.com/elastic/kibana/issues/35965 test_api_keys: { elasticsearch: { From b408a9fc9c3fe0cf9681e0d208a2cbe9888a245a Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 25 Jun 2020 22:52:31 +0200 Subject: [PATCH 61/82] delete testbed plugins (#69661) (#69990) * delete testbed plugins * remove FTR tests based on KP testbed --- src/dev/build/tasks/copy_source_task.js | 2 - src/legacy/core_plugins/testbed/README.md | 8 -- src/legacy/core_plugins/testbed/index.js | 30 ----- src/legacy/core_plugins/testbed/package.json | 4 - .../core_plugins/testbed/public/index.js | 20 --- .../core_plugins/testbed/public/testbed.html | 12 -- .../core_plugins/testbed/public/testbed.js | 29 ----- src/plugins/testbed/kibana.json | 8 -- src/plugins/testbed/public/index.ts | 25 ---- src/plugins/testbed/public/plugin.ts | 48 -------- src/plugins/testbed/server/index.ts | 114 ------------------ test/api_integration/apis/core/index.js | 13 -- 12 files changed, 313 deletions(-) delete mode 100644 src/legacy/core_plugins/testbed/README.md delete mode 100644 src/legacy/core_plugins/testbed/index.js delete mode 100644 src/legacy/core_plugins/testbed/package.json delete mode 100644 src/legacy/core_plugins/testbed/public/index.js delete mode 100644 src/legacy/core_plugins/testbed/public/testbed.html delete mode 100644 src/legacy/core_plugins/testbed/public/testbed.js delete mode 100644 src/plugins/testbed/kibana.json delete mode 100644 src/plugins/testbed/public/index.ts delete mode 100644 src/plugins/testbed/public/plugin.ts delete mode 100644 src/plugins/testbed/server/index.ts diff --git a/src/dev/build/tasks/copy_source_task.js b/src/dev/build/tasks/copy_source_task.js index ddc6d000bca19..32eb7bf8712e3 100644 --- a/src/dev/build/tasks/copy_source_task.js +++ b/src/dev/build/tasks/copy_source_task.js @@ -34,9 +34,7 @@ export const CopySourceTask = { '!src/test_utils/**', '!src/fixtures/**', '!src/legacy/core_plugins/tests_bundle/**', - '!src/legacy/core_plugins/testbed/**', '!src/legacy/core_plugins/console/public/tests/**', - '!src/plugins/testbed/**', '!src/cli/cluster/**', '!src/cli/repl/**', '!src/es_archiver/**', diff --git a/src/legacy/core_plugins/testbed/README.md b/src/legacy/core_plugins/testbed/README.md deleted file mode 100644 index ac50ffbb804b5..0000000000000 --- a/src/legacy/core_plugins/testbed/README.md +++ /dev/null @@ -1,8 +0,0 @@ -## Kibana Testbed - -Sometimes when developing for Kibana, it is useful to have an isolated routable space to demonstrate new functionality. This Testbed provides such a space. - -To make use of the testbed, edit the testbed.js, testbed.html, and testbed.less files as necessary. When you are done demonstrating -your new functionality, remember to cleanup your changes and restore the testbed to its pristine state for the next person. - -To access the testbed, visit `http://localhost:5601/app/kibana#/testbed` diff --git a/src/legacy/core_plugins/testbed/index.js b/src/legacy/core_plugins/testbed/index.js deleted file mode 100644 index f0b61ea0c3de7..0000000000000 --- a/src/legacy/core_plugins/testbed/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; - -export default function (kibana) { - return new kibana.Plugin({ - id: 'testbed', - publicDir: resolve(__dirname, 'public'), - uiExports: { - hacks: ['plugins/testbed'], - }, - }); -} diff --git a/src/legacy/core_plugins/testbed/package.json b/src/legacy/core_plugins/testbed/package.json deleted file mode 100644 index 98fcaf7eda95d..0000000000000 --- a/src/legacy/core_plugins/testbed/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "testbed", - "version": "kibana" -} \ No newline at end of file diff --git a/src/legacy/core_plugins/testbed/public/index.js b/src/legacy/core_plugins/testbed/public/index.js deleted file mode 100644 index c6687de249cf2..0000000000000 --- a/src/legacy/core_plugins/testbed/public/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import './testbed'; diff --git a/src/legacy/core_plugins/testbed/public/testbed.html b/src/legacy/core_plugins/testbed/public/testbed.html deleted file mode 100644 index 52455beb02360..0000000000000 --- a/src/legacy/core_plugins/testbed/public/testbed.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
- -
{{ testbed.data }}
- - - - - - -
-
diff --git a/src/legacy/core_plugins/testbed/public/testbed.js b/src/legacy/core_plugins/testbed/public/testbed.js deleted file mode 100644 index 13005a6106ca4..0000000000000 --- a/src/legacy/core_plugins/testbed/public/testbed.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import uiRoutes from 'ui/routes'; -import template from './testbed.html'; - -uiRoutes.when('/testbed', { - template: template, - controllerAs: 'testbed', - controller: class TestbedController { - constructor() {} - }, -}); diff --git a/src/plugins/testbed/kibana.json b/src/plugins/testbed/kibana.json deleted file mode 100644 index 9afe357b7a010..0000000000000 --- a/src/plugins/testbed/kibana.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "testbed", - "version": "0.0.1", - "kibanaVersion": "kibana", - "configPath": ["core", "testbed"], - "server": true, - "ui": true -} diff --git a/src/plugins/testbed/public/index.ts b/src/plugins/testbed/public/index.ts deleted file mode 100644 index 601db10f6f8bb..0000000000000 --- a/src/plugins/testbed/public/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; -import { TestbedPlugin, TestbedPluginSetup, TestbedPluginStart } from './plugin'; - -export const plugin: PluginInitializer = ( - initializerContext: PluginInitializerContext -) => new TestbedPlugin(initializerContext); diff --git a/src/plugins/testbed/public/plugin.ts b/src/plugins/testbed/public/plugin.ts deleted file mode 100644 index 8c70485d9ee8b..0000000000000 --- a/src/plugins/testbed/public/plugin.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/public'; - -interface ConfigType { - uiProp: string; -} - -export class TestbedPlugin implements Plugin { - constructor(private readonly initializerContext: PluginInitializerContext) {} - - public async setup(core: CoreSetup, deps: {}) { - const config = this.initializerContext.config.get(); - - // eslint-disable-next-line no-console - console.log(`Testbed plugin set up. uiProp: '${config.uiProp}'`); - return { - foo: 'bar', - }; - } - - public start() { - // eslint-disable-next-line no-console - console.log(`Testbed plugin started`); - } - - public stop() {} -} - -export type TestbedPluginSetup = ReturnType; -export type TestbedPluginStart = ReturnType; diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts deleted file mode 100644 index 21f97259c97f4..0000000000000 --- a/src/plugins/testbed/server/index.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { map } from 'rxjs/operators'; -import { schema, TypeOf } from '@kbn/config-schema'; - -import { - CoreSetup, - CoreStart, - Logger, - PluginInitializerContext, - PluginConfigDescriptor, - PluginName, -} from 'kibana/server'; - -const configSchema = schema.object({ - secret: schema.string({ defaultValue: 'Not really a secret :/' }), - uiProp: schema.string({ defaultValue: 'Accessible from client' }), -}); - -type ConfigType = TypeOf; - -export const config: PluginConfigDescriptor = { - exposeToBrowser: { - uiProp: true, - }, - schema: configSchema, - deprecations: ({ rename, unused, renameFromRoot }) => [ - rename('securityKey', 'secret'), - renameFromRoot('oldtestbed.uiProp', 'testbed.uiProp'), - unused('deprecatedProperty'), - ], -}; - -class Plugin { - private readonly log: Logger; - - constructor(private readonly initializerContext: PluginInitializerContext) { - this.log = this.initializerContext.logger.get(); - } - - public setup(core: CoreSetup, deps: Record) { - this.log.debug( - `Setting up TestBed with core contract [${Object.keys(core)}] and deps [${Object.keys(deps)}]` - ); - - const router = core.http.createRouter(); - router.get( - { path: '/requestcontext/elasticsearch', validate: false }, - async (context, req, res) => { - const response = await context.core.elasticsearch.legacy.client.callAsInternalUser('ping'); - return res.ok({ body: `Elasticsearch: ${response}` }); - } - ); - - router.get( - { path: '/requestcontext/savedobjectsclient', validate: false }, - async (context, req, res) => { - const response = await context.core.savedObjects.client.find({ type: 'TYPE' }); - return res.ok({ body: `SavedObjects client: ${JSON.stringify(response)}` }); - } - ); - - return { - data$: this.initializerContext.config.create().pipe( - map((configValue) => { - this.log.debug(`I've got value from my config: ${configValue.secret}`); - return `Some exposed data derived from config: ${configValue.secret}`; - }) - ), - pingElasticsearch: async () => { - const [coreStart] = await core.getStartServices(); - return coreStart.elasticsearch.legacy.client.callAsInternalUser('ping'); - }, - }; - } - - public start(core: CoreStart, deps: Record) { - this.log.debug( - `Starting up TestBed testbed with core contract [${Object.keys( - core - )}] and deps [${Object.keys(deps)}]` - ); - - return { - getStartContext() { - return core; - }, - }; - } - - public stop() { - this.log.debug(`Stopping TestBed`); - } -} - -export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); diff --git a/test/api_integration/apis/core/index.js b/test/api_integration/apis/core/index.js index c522acaea25a3..ab9bb8d33c2dc 100644 --- a/test/api_integration/apis/core/index.js +++ b/test/api_integration/apis/core/index.js @@ -22,19 +22,6 @@ export default function ({ getService }) { const supertest = getService('supertest'); describe('core', () => { - describe('request context', () => { - it('provides access to elasticsearch', async () => - await supertest.get('/requestcontext/elasticsearch').expect(200, 'Elasticsearch: true')); - - it('provides access to SavedObjects client', async () => - await supertest - .get('/requestcontext/savedobjectsclient') - .expect( - 200, - 'SavedObjects client: {"page":1,"per_page":20,"total":0,"saved_objects":[]}' - )); - }); - describe('compression', () => { it(`uses compression when there isn't a referer`, async () => { await supertest From 35fde530521fca177fdac545b016aa476aff413f Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Thu, 25 Jun 2020 16:42:47 -0500 Subject: [PATCH 62/82] [7.x] [Metrics UI] Prefill alerts from the global dropdown (#68967) (#69981) * [Metrics UI] Prefill alerts from the global dropdown (#68967) Co-authored-by: Elastic Machine * Fix uncaught typecheck merge conflict Co-authored-by: Elastic Machine --- .../inventory/components/alert_dropdown.tsx | 12 +- .../hooks/use_inventory_alert_prefill.ts | 24 ++ .../components/alert_dropdown.tsx | 12 +- .../components/expression.test.tsx | 119 ++++++++++ .../components/expression.tsx | 31 ++- .../components/validation.tsx | 2 +- .../use_metric_threshold_alert_prefill.ts | 34 +++ .../public/alerting/metric_threshold/types.ts | 9 + .../public/alerting/use_alert_prefill.ts | 18 ++ .../containers/with_kuery_autocompletion.tsx | 2 +- .../infra/public/pages/metrics/index.tsx | 216 +++++++++--------- .../hooks/use_waffle_filters.test.ts | 56 +++++ .../hooks/use_waffle_filters.ts | 5 + .../hooks/use_waffle_options.test.ts | 62 +++++ .../hooks/use_waffle_options.ts | 8 + .../use_metrics_explorer_options.test.tsx | 42 +++- .../hooks/use_metrics_explorer_options.ts | 18 +- .../common/expression_items/threshold.tsx | 4 +- 18 files changed, 539 insertions(+), 135 deletions(-) create mode 100644 x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx create mode 100644 x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts create mode 100644 x-pack/plugins/infra/public/alerting/use_alert_prefill.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts diff --git a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx index 47a0f037816bc..04642a01c15b4 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/inventory/components/alert_dropdown.tsx @@ -7,6 +7,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useAlertPrefillContext } from '../../../alerting/use_alert_prefill'; import { AlertFlyout } from './alert_flyout'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; @@ -15,6 +16,9 @@ export const InventoryAlertDropdown = () => { const [flyoutVisible, setFlyoutVisible] = useState(false); const kibana = useKibana(); + const { inventoryPrefill } = useAlertPrefillContext(); + const { nodeType, metric, filterQuery } = inventoryPrefill; + const closePopover = useCallback(() => { setPopoverOpen(false); }, [setPopoverOpen]); @@ -57,7 +61,13 @@ export const InventoryAlertDropdown = () => { > - + ); }; diff --git a/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts new file mode 100644 index 0000000000000..d659057b95ed9 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/inventory/hooks/use_inventory_alert_prefill.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; +import { SnapshotMetricInput } from '../../../../common/http_api/snapshot_api'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; + +export const useInventoryAlertPrefill = () => { + const [nodeType, setNodeType] = useState('host'); + const [filterQuery, setFilterQuery] = useState(); + const [metric, setMetric] = useState({ type: 'cpu' }); + + return { + nodeType, + filterQuery, + metric, + setNodeType, + setFilterQuery, + setMetric, + }; +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx index d26575f65dfec..384a93e796dbe 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx @@ -7,14 +7,18 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { AlertFlyout } from './alert_flyout'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useAlertPrefillContext } from '../../use_alert_prefill'; +import { AlertFlyout } from './alert_flyout'; export const MetricsAlertDropdown = () => { const [popoverOpen, setPopoverOpen] = useState(false); const [flyoutVisible, setFlyoutVisible] = useState(false); const kibana = useKibana(); + const { metricThresholdPrefill } = useAlertPrefillContext(); + const { groupBy, filterQuery, metrics } = metricThresholdPrefill; + const closePopover = useCallback(() => { setPopoverOpen(false); }, [setPopoverOpen]); @@ -57,7 +61,11 @@ export const MetricsAlertDropdown = () => { > - + ); }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx new file mode 100644 index 0000000000000..f6119107ac133 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.test.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { actionTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; +import { alertTypeRegistryMock } from '../../../../../triggers_actions_ui/public/application/alert_type_registry.mock'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +import { AlertContextMeta } from '../types'; +import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; +import React from 'react'; +import { Expressions } from './expression'; +import { act } from 'react-dom/test-utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Comparator } from '../../../../server/lib/alerting/metric_threshold/types'; + +jest.mock('../../../containers/source/use_source_via_http', () => ({ + useSourceViaHttp: () => ({ + source: { id: 'default' }, + createDerivedIndexPattern: () => ({ fields: [], title: 'metricbeat-*' }), + }), +})); + +describe('Expression', () => { + async function setup(currentOptions: { + metrics?: MetricsExplorerMetric[]; + filterQuery?: string; + groupBy?: string; + }) { + const alertParams = { + criteria: [], + groupBy: undefined, + filterQueryText: '', + }; + + const mocks = coreMock.createSetup(); + const startMocks = coreMock.createStart(); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + + const context: AlertsContextValue = { + http: mocks.http, + toastNotifications: mocks.notifications.toasts, + actionTypeRegistry: actionTypeRegistryMock.create() as any, + alertTypeRegistry: alertTypeRegistryMock.create() as any, + docLinks: startMocks.docLinks, + capabilities: { + ...capabilities, + actions: { + delete: true, + save: true, + show: true, + }, + }, + metadata: { + currentOptions, + }, + }; + + const wrapper = mountWithIntl( + Reflect.set(alertParams, key, value)} + setAlertProperty={() => {}} + /> + ); + + const update = async () => + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + await update(); + + return { wrapper, update, alertParams }; + } + + it('should prefill the alert using the context metadata', async () => { + const currentOptions = { + groupBy: 'host.hostname', + filterQuery: 'foo', + metrics: [ + { aggregation: 'avg', field: 'system.load.1' }, + { aggregation: 'cardinality', field: 'system.cpu.user.pct' }, + ] as MetricsExplorerMetric[], + }; + const { alertParams } = await setup(currentOptions); + expect(alertParams.groupBy).toBe('host.hostname'); + expect(alertParams.filterQueryText).toBe('foo'); + expect(alertParams.criteria).toEqual([ + { + metric: 'system.load.1', + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', + aggType: 'avg', + }, + { + metric: 'system.cpu.user.pct', + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', + aggType: 'cardinality', + }, + ]); + }); +}); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index 3c3351f4ddd76..f45474f284484 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { debounce, pick } from 'lodash'; +import { debounce, pick, omit } from 'lodash'; import { Unit } from '@elastic/datemath'; import * as rt from 'io-ts'; import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react'; @@ -52,7 +52,7 @@ import { useSourceViaHttp } from '../../../containers/source/use_source_via_http import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; import { ExpressionRow } from './expression_row'; -import { AlertContextMeta, TimeUnit, MetricExpression } from '../types'; +import { AlertContextMeta, TimeUnit, MetricExpression, AlertParams } from '../types'; import { ExpressionChart } from './expression_chart'; import { validateMetricThreshold } from './validation'; @@ -60,14 +60,7 @@ const FILTER_TYPING_DEBOUNCE_MS = 500; interface Props { errors: IErrorObject[]; - alertParams: { - criteria: MetricExpression[]; - groupBy?: string; - filterQuery?: string; - sourceId?: string; - filterQueryText?: string; - alertOnNoData?: boolean; - }; + alertParams: AlertParams; alertsContext: AlertsContextValue; alertInterval: string; setAlertParams(key: string, value: any): void; @@ -81,6 +74,7 @@ const defaultExpression = { timeSize: 1, timeUnit: 'm', } as MetricExpression; +export { defaultExpression }; export const Expressions: React.FC = (props) => { const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props; @@ -247,6 +241,13 @@ export const Expressions: React.FC = (props) => { } }, [alertsContext.metadata, derivedIndexPattern, setAlertParams]); + const preFillAlertGroupBy = useCallback(() => { + const md = alertsContext.metadata; + if (md && md.currentOptions?.groupBy && !md.series) { + setAlertParams('groupBy', md.currentOptions.groupBy); + } + }, [alertsContext.metadata, setAlertParams]); + const onSelectPreviewLookbackInterval = useCallback((e) => { setPreviewLookbackInterval(e.target.value); setPreviewResult(null); @@ -286,6 +287,10 @@ export const Expressions: React.FC = (props) => { preFillAlertFilter(); } + if (!alertParams.groupBy) { + preFillAlertGroupBy(); + } + if (!alertParams.sourceId) { setAlertParams('sourceId', source?.id || 'default'); } @@ -465,7 +470,7 @@ export const Expressions: React.FC = (props) => { id="selectPreviewLookbackInterval" value={previewLookbackInterval} onChange={onSelectPreviewLookbackInterval} - options={previewOptions} + options={previewDOMOptions} /> @@ -588,6 +593,10 @@ export const Expressions: React.FC = (props) => { ); }; +const previewDOMOptions: Array<{ text: string; value: string }> = previewOptions.map((o) => + omit(o, 'shortText') +); + // required for dynamic import // eslint-disable-next-line import/no-default-export export default Expressions; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx index da342f0a45420..2221d3cd4fe12 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx @@ -50,7 +50,7 @@ export function validateMetricThreshold({ if (!c.aggType) { errors[id].aggField.push( i18n.translate('xpack.infra.metrics.alertFlyout.error.aggregationRequired', { - defaultMessage: 'Aggreation is required.', + defaultMessage: 'Aggregation is required.', }) ); } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts new file mode 100644 index 0000000000000..366d6aa7003e6 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metric_threshold_alert_prefill.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual } from 'lodash'; +import { useState } from 'react'; +import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; + +interface MetricThresholdPrefillOptions { + groupBy: string | string[] | undefined; + filterQuery: string | undefined; + metrics: MetricsExplorerMetric[]; +} + +export const useMetricThresholdAlertPrefill = () => { + const [prefillOptionsState, setPrefillOptionsState] = useState({ + groupBy: undefined, + filterQuery: undefined, + metrics: [], + }); + + const { groupBy, filterQuery, metrics } = prefillOptionsState; + + return { + groupBy, + filterQuery, + metrics, + setPrefillOptions(newState: MetricThresholdPrefillOptions) { + if (!isEqual(newState, prefillOptionsState)) setPrefillOptionsState(newState); + }, + }; +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts index feeec4b0ce8bf..2f8d7ec0ba6f4 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -51,3 +51,12 @@ export interface ExpressionChartData { id: string; series: ExpressionChartSeries; } + +export interface AlertParams { + criteria: MetricExpression[]; + groupBy?: string; + filterQuery?: string; + sourceId?: string; + filterQueryText?: string; + alertOnNoData?: boolean; +} diff --git a/x-pack/plugins/infra/public/alerting/use_alert_prefill.ts b/x-pack/plugins/infra/public/alerting/use_alert_prefill.ts new file mode 100644 index 0000000000000..eff2fe462509f --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/use_alert_prefill.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import createContainer from 'constate'; +import { useMetricThresholdAlertPrefill } from './metric_threshold/hooks/use_metric_threshold_alert_prefill'; +import { useInventoryAlertPrefill } from './inventory/hooks/use_inventory_alert_prefill'; + +const useAlertPrefill = () => { + const metricThresholdPrefill = useMetricThresholdAlertPrefill(); + const inventoryPrefill = useInventoryAlertPrefill(); + + return { metricThresholdPrefill, inventoryPrefill }; +}; + +export const [AlertPrefillProvider, useAlertPrefillContext] = createContainer(useAlertPrefill); diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx index a04897d9c738d..2c76b3bb925ee 100644 --- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -59,7 +59,7 @@ class WithKueryAutocompletionComponent extends React.Component< ) => { const { indexPattern } = this.props; const language = 'kuery'; - const hasQuerySuggestions = this.props.kibana.services.data.autocomplete.hasQuerySuggestions( + const hasQuerySuggestions = this.props.kibana.services.data?.autocomplete.hasQuerySuggestions( language ); diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index ab7f41e3066b8..121748f8e5220 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -31,6 +31,7 @@ import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters import { InventoryAlertDropdown } from '../../alerting/inventory/components/alert_dropdown'; import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; +import { AlertPrefillProvider } from '../../alerting/use_alert_prefill'; const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLabel', { defaultMessage: 'Add data', @@ -44,114 +45,119 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { return ( - - - - - - - + + + + + + -
- - - - - - - - - - - - {ADD_DATA_LABEL} - - - - + - - - ( - - {({ configuration, createDerivedIndexPattern }) => ( - - - {configuration ? ( - - ) : ( - - )} - - )} - - )} +
- - - - - - + + + + + + + + + + + + {ADD_DATA_LABEL} + + + + + + + + ( + + {({ configuration, createDerivedIndexPattern }) => ( + + + {configuration ? ( + + ) : ( + + )} + + )} + + )} + /> + + + + + + + ); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts new file mode 100644 index 0000000000000..93b6b635183dd --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useWaffleFilters, WaffleFiltersState } from './use_waffle_filters'; + +// Mock useUrlState hook +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + location: '', + replace: () => {}, + }), +})); + +jest.mock('../../../../containers/source', () => ({ + useSourceContext: () => ({ + createDerivedIndexPattern: () => 'jestbeat-*', + }), +})); + +let PREFILL: Record = {}; +jest.mock('../../../../alerting/use_alert_prefill', () => ({ + useAlertPrefillContext: () => ({ + inventoryPrefill: { + setFilterQuery(filterQuery: string) { + PREFILL = { filterQuery }; + }, + }, + }), +})); + +const renderUseWaffleFiltersHook = () => renderHook(() => useWaffleFilters()); + +describe('useWaffleFilters', () => { + beforeEach(() => { + PREFILL = {}; + }); + + it('should sync the options to the inventory alert preview context', () => { + const { result, rerender } = renderUseWaffleFiltersHook(); + + const newQuery = { + expression: 'foo', + kind: 'kuery', + } as WaffleFiltersState; + act(() => { + result.current.applyFilterQuery(newQuery); + }); + rerender(); + expect(PREFILL.filterQuery).toEqual(newQuery.expression); + }); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts index 63d9d08796f05..d4fb1356be77e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts @@ -10,6 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import createContainter from 'constate'; +import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; import { useUrlState } from '../../../../utils/use_url_state'; import { useSourceContext } from '../../../../containers/source'; import { convertKueryToElasticSearchQuery } from '../../../../utils/kuery'; @@ -68,6 +69,10 @@ export const useWaffleFilters = () => { filterQueryDraft, ]); + const { inventoryPrefill } = useAlertPrefillContext(); + const prefillContext = useMemo(() => inventoryPrefill, [inventoryPrefill]); // For Jest compatibility + useEffect(() => prefillContext.setFilterQuery(state.expression), [prefillContext, state]); + return { filterQuery: urlState, filterQueryDraft, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts new file mode 100644 index 0000000000000..579073e9500d0 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useWaffleOptions, WaffleOptionsState } from './use_waffle_options'; + +// Mock useUrlState hook +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + location: '', + replace: () => {}, + }), +})); + +// Jest can't access variables outside the scope of the mock factory function except to +// reassign them, so we can't make these both part of the same object +let PREFILL_NODETYPE: WaffleOptionsState['nodeType'] | undefined; +let PREFILL_METRIC: WaffleOptionsState['metric'] | undefined; +jest.mock('../../../../alerting/use_alert_prefill', () => ({ + useAlertPrefillContext: () => ({ + inventoryPrefill: { + setNodeType(nodeType: WaffleOptionsState['nodeType']) { + PREFILL_NODETYPE = nodeType; + }, + setMetric(metric: WaffleOptionsState['metric']) { + PREFILL_METRIC = metric; + }, + }, + }), +})); + +const renderUseWaffleOptionsHook = () => renderHook(() => useWaffleOptions()); + +describe('useWaffleOptions', () => { + beforeEach(() => { + PREFILL_NODETYPE = undefined; + PREFILL_METRIC = undefined; + }); + + it('should sync the options to the inventory alert preview context', () => { + const { result, rerender } = renderUseWaffleOptionsHook(); + + const newOptions = { + nodeType: 'pod', + metric: { type: 'memory' }, + } as WaffleOptionsState; + act(() => { + result.current.changeNodeType(newOptions.nodeType); + }); + rerender(); + expect(PREFILL_NODETYPE).toEqual(newOptions.nodeType); + act(() => { + result.current.changeMetric(newOptions.metric); + }); + rerender(); + expect(PREFILL_METRIC).toEqual(newOptions.metric); + }); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts index 975e33cf2415f..a3132c8384979 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts @@ -10,6 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import createContainer from 'constate'; +import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; import { InventoryColorPaletteRT } from '../../../../lib/lib'; import { SnapshotMetricInput, @@ -121,6 +122,13 @@ export const useWaffleOptions = () => { [setState] ); + const { inventoryPrefill } = useAlertPrefillContext(); + useEffect(() => { + const { setNodeType, setMetric } = inventoryPrefill; + setNodeType(state.nodeType); + setMetric(state.metric); + }, [state, inventoryPrefill]); + return { ...DEFAULT_WAFFLE_OPTIONS_STATE, ...state, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx index 1381ed9da656a..c35e9f17bdcc3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx @@ -4,26 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { useMetricsExplorerOptions, - MetricsExplorerOptionsContainer, MetricsExplorerOptions, MetricsExplorerTimeOptions, DEFAULT_OPTIONS, DEFAULT_TIMERANGE, } from './use_metrics_explorer_options'; -const renderUseMetricsExplorerOptionsHook = () => - renderHook(() => useMetricsExplorerOptions(), { - initialProps: {}, - wrapper: ({ children }) => ( - - {children} - - ), - }); +let PREFILL: Record = {}; +jest.mock('../../../../alerting/use_alert_prefill', () => ({ + useAlertPrefillContext: () => ({ + metricThresholdPrefill: { + setPrefillOptions(opts: Record) { + PREFILL = opts; + }, + }, + }), +})); + +const renderUseMetricsExplorerOptionsHook = () => renderHook(() => useMetricsExplorerOptions()); interface LocalStore { [key: string]: string; @@ -52,6 +53,7 @@ describe('useMetricExplorerOptions', () => { beforeEach(() => { delete STORE.MetricsExplorerOptions; delete STORE.MetricsExplorerTimeRange; + PREFILL = {}; }); it('should just work', () => { @@ -100,4 +102,22 @@ describe('useMetricExplorerOptions', () => { const { result } = renderUseMetricsExplorerOptionsHook(); expect(result.current.options).toEqual(newOptions); }); + + it('should sync the options to the threshold alert preview context', () => { + const { result, rerender } = renderUseMetricsExplorerOptionsHook(); + + const newOptions: MetricsExplorerOptions = { + ...DEFAULT_OPTIONS, + metrics: [{ aggregation: 'count' }], + filterQuery: 'foo', + groupBy: 'host.hostname', + }; + act(() => { + result.current.setOptions(newOptions); + }); + rerender(); + expect(PREFILL.metrics).toEqual(newOptions.metrics); + expect(PREFILL.groupBy).toEqual(newOptions.groupBy); + expect(PREFILL.filterQuery).toEqual(newOptions.filterQuery); + }); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts index 56595c09aadde..8abdffd39ed3a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts @@ -5,7 +5,8 @@ */ import createContainer from 'constate'; -import { useState, useEffect, Dispatch, SetStateAction } from 'react'; +import { useState, useEffect, useMemo, Dispatch, SetStateAction } from 'react'; +import { useAlertPrefillContext } from '../../../../alerting/use_alert_prefill'; import { MetricsExplorerColor } from '../../../../../common/color_palette'; import { MetricsExplorerAggregation, @@ -122,6 +123,21 @@ export const useMetricsExplorerOptions = () => { DEFAULT_CHART_OPTIONS ); const [isAutoReloading, setAutoReloading] = useState(false); + + const { metricThresholdPrefill } = useAlertPrefillContext(); + // For Jest compatibility; including metricThresholdPrefill as a dep in useEffect causes an + // infinite loop in test environment + const prefillContext = useMemo(() => metricThresholdPrefill, [metricThresholdPrefill]); + + useEffect(() => { + if (prefillContext) { + const { setPrefillOptions } = prefillContext; + const { metrics, groupBy, filterQuery } = options; + + setPrefillOptions({ metrics, groupBy, filterQuery }); + } + }, [options, prefillContext]); + return { defaultViewState: { options: DEFAULT_OPTIONS, diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx index 09acf4fe1ef68..fe592aadb37a5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx @@ -136,14 +136,14 @@ export const ThresholdExpression = ({ ) : null} 0 || !threshold[i]} + isInvalid={errors[`threshold${i}`]?.length > 0 || !threshold[i]} error={errors[`threshold${i}`]} > 0 || !threshold[i]} + isInvalid={errors[`threshold${i}`]?.length > 0 || !threshold[i]} onChange={(e) => { const { value } = e.target; const thresholdVal = value !== '' ? parseFloat(value) : undefined; From 836f2f7e54a32dfdd24ef0aee18403ddcef181a1 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 25 Jun 2020 18:40:55 -0400 Subject: [PATCH 63/82] Rename Resolver types to include 'Resolver' (#69926) (#69996) Include the word 'Resolver' in some Resolver specific types in order to improve readability and ease of auto-importing. --- .../security_solution/common/endpoint/types.ts | 8 ++++---- .../public/resolver/store/middleware.ts | 6 +++--- .../routes/resolver/utils/children_helper.ts | 10 +++++++--- .../endpoint/routes/resolver/utils/fetch.ts | 6 +++--- .../endpoint/routes/resolver/utils/node.ts | 11 +++++++---- .../api_integration/apis/endpoint/resolver.ts | 18 +++++++++++------- 6 files changed, 35 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index 4f13fd97ce442..42f5f4b220da9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -74,7 +74,7 @@ export interface ResolverNodeStats { /** * A child node can also have additional children so we need to provide a pagination cursor. */ -export interface ChildNode extends LifecycleNode { +export interface ResolverChildNode extends ResolverLifecycleNode { /** * A child node's pagination cursor can be null for a couple reasons: * 1. At the time of querying it could have no children in ES, in which case it will be marked as @@ -89,7 +89,7 @@ export interface ChildNode extends LifecycleNode { * has an array of lifecycle events. */ export interface ResolverChildren { - childNodes: ChildNode[]; + childNodes: ResolverChildNode[]; /** * This is the children cursor for the origin of a tree. */ @@ -116,7 +116,7 @@ export interface ResolverTree { /** * The lifecycle events (start, end etc) for a node. */ -export interface LifecycleNode { +export interface ResolverLifecycleNode { entityID: string; lifecycle: ResolverEvent[]; /** @@ -132,7 +132,7 @@ export interface ResolverAncestry { /** * An array of ancestors with the lifecycle events grouped together */ - ancestors: LifecycleNode[]; + ancestors: ResolverLifecycleNode[]; /** * A cursor for retrieving additional ancestors for a particular node. `null` indicates that there were no additional * ancestors when the request returned. More could have been ingested by ES after the fact though. diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts index a352a076e5a97..343b4e1a14478 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware.ts @@ -12,7 +12,7 @@ import { ResolverEvent, ResolverChildren, ResolverAncestry, - LifecycleNode, + ResolverLifecycleNode, ResolverNodeStats, ResolverRelatedEvents, } from '../../../common/endpoint/types'; @@ -25,10 +25,10 @@ type MiddlewareFactory = ( ) => (next: Dispatch) => (action: ResolverAction) => unknown; function getLifecycleEventsAndStats( - nodes: LifecycleNode[], + nodes: ResolverLifecycleNode[], stats: Map ): ResolverEvent[] { - return nodes.reduce((flattenedEvents: ResolverEvent[], currentNode: LifecycleNode) => { + return nodes.reduce((flattenedEvents: ResolverEvent[], currentNode: ResolverLifecycleNode) => { if (currentNode.lifecycle && currentNode.lifecycle.length > 0) { flattenedEvents.push(...currentNode.lifecycle); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts index 7a3e1fc591e82..e60e5087c30a9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts @@ -9,7 +9,11 @@ import { parentEntityId, isProcessStart, } from '../../../../../common/endpoint/models/event'; -import { ChildNode, ResolverEvent, ResolverChildren } from '../../../../../common/endpoint/types'; +import { + ResolverChildNode, + ResolverEvent, + ResolverChildren, +} from '../../../../../common/endpoint/types'; import { PaginationBuilder } from './pagination'; import { createChild } from './node'; @@ -17,7 +21,7 @@ import { createChild } from './node'; * This class helps construct the children structure when building a resolver tree. */ export class ChildrenNodesHelper { - private readonly cache: Map = new Map(); + private readonly cache: Map = new Map(); constructor(private readonly rootID: string) { this.cache.set(rootID, createChild(rootID)); @@ -27,7 +31,7 @@ export class ChildrenNodesHelper { * Constructs a ResolverChildren response based on the children that were previously add. */ getNodes(): ResolverChildren { - const cacheCopy: Map = new Map(this.cache); + const cacheCopy: Map = new Map(this.cache); const rootNode = cacheCopy.get(this.rootID); let rootNextChild = null; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts index d448649ae447b..0af2fca7106be 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts @@ -10,7 +10,7 @@ import { ResolverRelatedEvents, ResolverAncestry, ResolverRelatedAlerts, - LifecycleNode, + ResolverLifecycleNode, ResolverEvent, } from '../../../../../common/endpoint/types'; import { @@ -143,7 +143,7 @@ export class Fetcher { return tree; } - private async getNode(entityID: string): Promise { + private async getNode(entityID: string): Promise { const query = new LifecycleQuery(this.eventsIndexPattern, this.endpointID); const results = await query.search(this.client, entityID); if (results.length === 0) { @@ -186,7 +186,7 @@ export class Fetcher { // bucket the start and end events together for a single node const ancestryNodes = results.reduce( - (nodes: Map, ancestorEvent: ResolverEvent) => { + (nodes: Map, ancestorEvent: ResolverEvent) => { const nodeId = entityId(ancestorEvent); let node = nodes.get(nodeId); if (!node) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts index 58aa9efc1fc56..57a2ebfcc1792 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts @@ -7,10 +7,10 @@ import { ResolverEvent, ResolverAncestry, - LifecycleNode, + ResolverLifecycleNode, ResolverRelatedEvents, ResolverTree, - ChildNode, + ResolverChildNode, ResolverRelatedAlerts, } from '../../../../../common/endpoint/types'; @@ -49,7 +49,7 @@ export function createRelatedAlerts( * * @param entityID the entity_id of the child */ -export function createChild(entityID: string): ChildNode { +export function createChild(entityID: string): ResolverChildNode { const lifecycle = createLifecycle(entityID, []); return { ...lifecycle, @@ -70,7 +70,10 @@ export function createAncestry(): ResolverAncestry { * @param id the entity_id that these lifecycle nodes should have * @param lifecycle an array of lifecycle events */ -export function createLifecycle(entityID: string, lifecycle: ResolverEvent[]): LifecycleNode { +export function createLifecycle( + entityID: string, + lifecycle: ResolverEvent[] +): ResolverLifecycleNode { return { entityID, lifecycle }; } diff --git a/x-pack/test/api_integration/apis/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts index 67b828b8df30e..eeca8ee54e32f 100644 --- a/x-pack/test/api_integration/apis/endpoint/resolver.ts +++ b/x-pack/test/api_integration/apis/endpoint/resolver.ts @@ -6,8 +6,8 @@ import _ from 'lodash'; import expect from '@kbn/expect'; import { - ChildNode, - LifecycleNode, + ResolverChildNode, + ResolverLifecycleNode, ResolverAncestry, ResolverEvent, ResolverRelatedEvents, @@ -35,7 +35,7 @@ import { Options, GeneratedTrees } from '../../services/resolver'; * @param node a lifecycle node containing the start and end events for a node * @param nodeMap a map of entity_ids to nodes to look for the passed in `node` */ -const expectLifecycleNodeInMap = (node: LifecycleNode, nodeMap: Map) => { +const expectLifecycleNodeInMap = (node: ResolverLifecycleNode, nodeMap: Map) => { const genNode = nodeMap.get(node.entityID); expect(genNode).to.be.ok(); compareArrays(genNode!.lifecycle, node.lifecycle, true); @@ -49,7 +49,11 @@ const expectLifecycleNodeInMap = (node: LifecycleNode, nodeMap: Map { +const verifyAncestry = ( + ancestors: ResolverLifecycleNode[], + tree: Tree, + verifyLastParent: boolean +) => { // group the ancestors by their entity_id mapped to a lifecycle node const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); // group by parent entity_id @@ -97,7 +101,7 @@ const verifyAncestry = (ancestors: LifecycleNode[], tree: Tree, verifyLastParent * * @param ancestors an array of ancestor nodes */ -const retrieveDistantAncestor = (ancestors: LifecycleNode[]) => { +const retrieveDistantAncestor = (ancestors: ResolverLifecycleNode[]) => { // group the ancestors by their entity_id mapped to a lifecycle node const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); let node = ancestors[0]; @@ -124,7 +128,7 @@ const retrieveDistantAncestor = (ancestors: LifecycleNode[]) => { * @param childrenPerParent an optional number to compare that there are a certain number of children for each parent */ const verifyChildren = ( - children: ChildNode[], + children: ResolverChildNode[], tree: Tree, numberOfParents?: number, childrenPerParent?: number @@ -210,7 +214,7 @@ const verifyStats = ( * @param categories the related event info used when generating the resolver tree */ const verifyLifecycleStats = ( - nodes: LifecycleNode[], + nodes: ResolverLifecycleNode[], categories: RelatedEventInfo[], relatedAlerts: number ) => { From c317540b3dd47d62504561280e80a79ca1203431 Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Thu, 25 Jun 2020 18:22:29 -0500 Subject: [PATCH 64/82] [Discover] set minBarHeight for high cardinality data (#69875) (#70012) --- .../discover/public/application/angular/directives/histogram.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/discover/public/application/angular/directives/histogram.tsx b/src/plugins/discover/public/application/angular/directives/histogram.tsx index 8b646106fe52f..9afe5e48bc5b8 100644 --- a/src/plugins/discover/public/application/angular/directives/histogram.tsx +++ b/src/plugins/discover/public/application/angular/directives/histogram.tsx @@ -323,6 +323,7 @@ export class DiscoverHistogram extends Component Date: Fri, 12 Jun 2020 09:35:19 -0500 Subject: [PATCH 65/82] [docker] add spaces settings --- .../os_packages/docker_generator/resources/bin/kibana-docker | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 13c13621ba071..d9d0ce49971eb 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -240,6 +240,8 @@ kibana_vars=( xpack.security.public.protocol xpack.security.public.hostname xpack.security.public.port + xpack.spaces.enabled + xpack.spaces.maxSpaces telemetry.allowChangingOptInStatus telemetry.enabled telemetry.optIn From 40bad84e4c0bd15b4d5d2dea96d47b664ad56998 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Thu, 25 Jun 2020 16:50:25 -0700 Subject: [PATCH 66/82] [7.x] Adds link for Cloud deployment settings (#66486) (#70015) Co-authored-by: Michail Yasonik Co-authored-by: Elastic Machine --- src/core/public/chrome/chrome_service.mock.ts | 3 + src/core/public/chrome/chrome_service.test.ts | 21 + src/core/public/chrome/chrome_service.tsx | 20 +- .../collapsible_nav.test.tsx.snap | 395 ++++++++++++++++ .../header/__snapshots__/header.test.tsx.snap | 435 ++++++++++++++++++ .../chrome/ui/header/collapsible_nav.test.tsx | 3 + .../chrome/ui/header/collapsible_nav.tsx | 36 +- .../public/chrome/ui/header/header.test.tsx | 8 + src/core/public/chrome/ui/header/header.tsx | 2 + src/core/public/chrome/ui/header/nav_link.tsx | 5 +- src/core/public/public.api.md | 2 + x-pack/.i18nrc.json | 1 + x-pack/plugins/cloud/public/plugin.ts | 20 +- x-pack/plugins/cloud/server/config.ts | 2 + 14 files changed, 948 insertions(+), 5 deletions(-) diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 4a79dd8869c1c..c9a05ff4e08fe 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -74,6 +74,8 @@ const createStartContractMock = () => { setHelpSupportUrl: jest.fn(), getIsNavDrawerLocked$: jest.fn(), getNavType$: jest.fn(), + getCustomNavLink$: jest.fn(), + setCustomNavLink: jest.fn(), }; startContract.navLinks.getAll.mockReturnValue([]); startContract.getBrand$.mockReturnValue(new BehaviorSubject({} as ChromeBrand)); @@ -81,6 +83,7 @@ const createStartContractMock = () => { startContract.getApplicationClasses$.mockReturnValue(new BehaviorSubject(['class-name'])); startContract.getBadge$.mockReturnValue(new BehaviorSubject({} as ChromeBadge)); startContract.getBreadcrumbs$.mockReturnValue(new BehaviorSubject([{} as ChromeBreadcrumb])); + startContract.getCustomNavLink$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined)); startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false)); startContract.getNavType$.mockReturnValue(new BehaviorSubject('modern' as NavType)); diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index e39733cc10de7..8dc81dceaccd6 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -363,6 +363,27 @@ describe('start', () => { }); }); + describe('custom nav link', () => { + it('updates/emits the current custom nav link', async () => { + const { chrome, service } = await start(); + const promise = chrome.getCustomNavLink$().pipe(toArray()).toPromise(); + + chrome.setCustomNavLink({ title: 'Manage cloud deployment' }); + chrome.setCustomNavLink(undefined); + service.stop(); + + await expect(promise).resolves.toMatchInlineSnapshot(` + Array [ + undefined, + Object { + "title": "Manage cloud deployment", + }, + undefined, + ] + `); + }); + }); + describe('help extension', () => { it('updates/emits the current help extension', async () => { const { chrome, service } = await start(); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 67cd43f0647e4..0fe3c1f083cf0 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -34,7 +34,7 @@ import { IUiSettingsClient } from '../ui_settings'; import { KIBANA_ASK_ELASTIC_LINK } from './constants'; import { ChromeDocTitle, DocTitleService } from './doc_title'; import { ChromeNavControls, NavControlsService } from './nav_controls'; -import { ChromeNavLinks, NavLinksService } from './nav_links'; +import { ChromeNavLinks, NavLinksService, ChromeNavLink } from './nav_links'; import { ChromeRecentlyAccessed, RecentlyAccessedService } from './recently_accessed'; import { Header } from './ui'; import { NavType } from './ui/header'; @@ -148,6 +148,7 @@ export class ChromeService { const helpExtension$ = new BehaviorSubject(undefined); const breadcrumbs$ = new BehaviorSubject([]); const badge$ = new BehaviorSubject(undefined); + const customNavLink$ = new BehaviorSubject(undefined); const helpSupportUrl$ = new BehaviorSubject(KIBANA_ASK_ELASTIC_LINK); const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true'); @@ -221,6 +222,7 @@ export class ChromeService { badge$={badge$.pipe(takeUntil(this.stop$))} basePath={http.basePath} breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))} + customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))} kibanaDocLink={docLinks.links.kibana} forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()} helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))} @@ -297,6 +299,12 @@ export class ChromeService { getIsNavDrawerLocked$: () => getIsNavDrawerLocked$, getNavType$: () => getNavType$, + + getCustomNavLink$: () => customNavLink$.pipe(takeUntil(this.stop$)), + + setCustomNavLink: (customNavLink?: ChromeNavLink) => { + customNavLink$.next(customNavLink); + }, }; } @@ -423,6 +431,16 @@ export interface ChromeStart { */ setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; + /** + * Get an observable of the current custom nav link + */ + getCustomNavLink$(): Observable | undefined>; + + /** + * Override the current set of custom nav link + */ + setCustomNavLink(newCustomNavLink?: Partial): void; + /** * Get an observable of the current custom help conttent */ diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 9239811df2065..9fee7b50f371b 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -61,6 +61,64 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` } } closeNav={[Function]} + customNavLink$={ + BehaviorSubject { + "_isScalar": false, + "_value": Object { + "baseUrl": "/", + "category": undefined, + "data-test-subj": "Custom link", + "href": "Custom link", + "id": "Custom link", + "isActive": true, + "legacy": false, + "title": "Custom link", + }, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], + "thrownError": null, + } + } homeHref="/" id="collapsibe-nav" isLocked={false} @@ -408,6 +466,46 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` data-test-subj="collapsibleNav" id="collapsibe-nav" > + +
+ +
+ +
+ +
+
+ + + +
+
+
+
+
+ +
+
+
+
+
+
    +
  • + +
  • +
+
+
+
+
+
+
+
+
    +
  • + +
  • +
+
+
+
+
+ +
+ +
+
+ +
    + +
  • + +
  • +
    +
+
+
+
+
+
+
+ +
+
{}, closeNav: () => {}, navigateToApp: () => Promise.resolve(), + customNavLink$: new BehaviorSubject(undefined), }; } @@ -120,12 +121,14 @@ describe('CollapsibleNav', () => { mockRecentNavLink({ label: 'recent 1' }), mockRecentNavLink({ label: 'recent 2' }), ]; + const customNavLink = mockLink({ title: 'Custom link' }); const component = mount( ); expect(component).toMatchSnapshot(); diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index 3800e30c90cd6..5abd14312f4a6 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -30,7 +30,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { groupBy, sortBy } from 'lodash'; -import React, { useRef } from 'react'; +import React, { Fragment, useRef } from 'react'; import { useObservable } from 'react-use'; import * as Rx from 'rxjs'; import { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '../..'; @@ -88,6 +88,7 @@ interface Props { onIsLockedUpdate: OnIsLockedUpdate; closeNav: () => void; navigateToApp: InternalApplicationStart['navigateToApp']; + customNavLink$: Rx.Observable; } export function CollapsibleNav({ @@ -105,6 +106,7 @@ export function CollapsibleNav({ }: Props) { const navLinks = useObservable(observables.navLinks$, []).filter((link) => !link.hidden); const recentlyAccessed = useObservable(observables.recentlyAccessed$, []); + const customNavLink = useObservable(observables.customNavLink$, undefined); const appId = useObservable(observables.appId$, ''); const lockRef = useRef(null); const groupedNavLinks = groupBy(navLinks, (link) => link?.category?.id); @@ -134,6 +136,38 @@ export function CollapsibleNav({ isDocked={isLocked} onClose={closeNav} > + {customNavLink && ( + + + + + + + + + + )} + {/* Pinned items */} { const navLinks$ = new BehaviorSubject([ { id: 'kibana', title: 'kibana', baseUrl: '', legacy: false }, ]); + const customNavLink$ = new BehaviorSubject({ + id: 'cloud-deployment-link', + title: 'Manage cloud deployment', + baseUrl: '', + legacy: false, + }); const recentlyAccessed$ = new BehaviorSubject([ { link: '', label: 'dashboard', id: 'dashboard' }, ]); @@ -87,6 +94,7 @@ describe('Header', () => { recentlyAccessed$={recentlyAccessed$} isLocked$={isLocked$} navType$={navType$} + customNavLink$={customNavLink$} /> ); expect(component).toMatchSnapshot(); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index d24b342e0386b..3da3caaaa4a4f 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -58,6 +58,7 @@ export interface HeaderProps { appTitle$: Observable; badge$: Observable; breadcrumbs$: Observable; + customNavLink$: Observable; homeHref: string; isVisible$: Observable; kibanaDocLink: string; @@ -203,6 +204,7 @@ export function Header({ toggleCollapsibleNavRef.current.focus(); } }} + customNavLink$={observables.customNavLink$} /> ) : ( // TODO #64541 diff --git a/src/core/public/chrome/ui/header/nav_link.tsx b/src/core/public/chrome/ui/header/nav_link.tsx index de3a6da37587e..cd47645f7f7b9 100644 --- a/src/core/public/chrome/ui/header/nav_link.tsx +++ b/src/core/public/chrome/ui/header/nav_link.tsx @@ -30,11 +30,12 @@ export const isModifiedOrPrevented = (event: React.MouseEvent {}, navigateToApp, dataTestSubj, + externalLink = false, }: Props) { const { legacy, active, id, title, disabled, euiIconType, icon, tooltip, href } = link; @@ -62,6 +64,7 @@ export function createEuiListItem({ } if ( + !externalLink && // ignore external links !legacyMode && // ignore when in legacy mode !legacy && // ignore links to legacy apps event.button === 0 && // ignore everything but left clicks diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6328a3eae7310..d10e351f4d13e 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -467,6 +467,7 @@ export interface ChromeStart { getBadge$(): Observable; getBrand$(): Observable; getBreadcrumbs$(): Observable; + getCustomNavLink$(): Observable | undefined>; getHelpExtension$(): Observable; getIsNavDrawerLocked$(): Observable; getIsVisible$(): Observable; @@ -479,6 +480,7 @@ export interface ChromeStart { setBadge(badge?: ChromeBadge): void; setBrand(brand: ChromeBrand): void; setBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]): void; + setCustomNavLink(newCustomNavLink?: Partial): void; setHelpExtension(helpExtension?: ChromeHelpExtension): void; setHelpSupportUrl(url: string): void; setIsVisible(isVisible: boolean): void; diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 36cfdf904d6d4..596ba17d343c0 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -8,6 +8,7 @@ "xpack.apm": ["legacy/plugins/apm", "plugins/apm"], "xpack.beatsManagement": ["legacy/plugins/beats_management", "plugins/beats_management"], "xpack.canvas": "plugins/canvas", + "xpack.cloud": "plugins/cloud", "xpack.dashboard": "plugins/dashboard_enhanced", "xpack.discover": "plugins/discover_enhanced", "xpack.crossClusterReplication": "plugins/cross_cluster_replication", diff --git a/x-pack/plugins/cloud/public/plugin.ts b/x-pack/plugins/cloud/public/plugin.ts index 62e21392f7110..1c3a770da79f5 100644 --- a/x-pack/plugins/cloud/public/plugin.ts +++ b/x-pack/plugins/cloud/public/plugin.ts @@ -5,6 +5,7 @@ */ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; import { getIsCloudEnabled } from '../common/is_cloud_enabled'; import { ELASTIC_SUPPORT_LINK } from '../common/constants'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; @@ -12,6 +13,7 @@ import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; interface CloudConfigType { id?: string; resetPasswordUrl?: string; + deploymentUrl?: string; } interface CloudSetupDependencies { @@ -24,10 +26,14 @@ export interface CloudSetup { } export class CloudPlugin implements Plugin { - constructor(private readonly initializerContext: PluginInitializerContext) {} + private config!: CloudConfigType; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.config = this.initializerContext.config.get(); + } public async setup(core: CoreSetup, { home }: CloudSetupDependencies) { - const { id, resetPasswordUrl } = this.initializerContext.config.get(); + const { id, resetPasswordUrl } = this.config; const isCloudEnabled = getIsCloudEnabled(id); if (home) { @@ -44,6 +50,16 @@ export class CloudPlugin implements Plugin { } public start(coreStart: CoreStart) { + const { deploymentUrl } = this.config; coreStart.chrome.setHelpSupportUrl(ELASTIC_SUPPORT_LINK); + if (deploymentUrl) { + coreStart.chrome.setCustomNavLink({ + title: i18n.translate('xpack.cloud.deploymentLinkLabel', { + defaultMessage: 'Manage this deployment', + }), + euiIconType: 'arrowLeft', + href: deploymentUrl, + }); + } } } diff --git a/x-pack/plugins/cloud/server/config.ts b/x-pack/plugins/cloud/server/config.ts index d899b45aebdfe..ff8a2c5acdf9a 100644 --- a/x-pack/plugins/cloud/server/config.ts +++ b/x-pack/plugins/cloud/server/config.ts @@ -22,6 +22,7 @@ const configSchema = schema.object({ id: schema.maybe(schema.string()), apm: schema.maybe(apmConfigSchema), resetPasswordUrl: schema.maybe(schema.string()), + deploymentUrl: schema.maybe(schema.string()), }); export type CloudConfigType = TypeOf; @@ -30,6 +31,7 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { id: true, resetPasswordUrl: true, + deploymentUrl: true, }, schema: configSchema, }; From 9ae8ffdea7e394c2a0cfa33d3b1070f8793c6818 Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Fri, 26 Jun 2020 09:41:25 +0300 Subject: [PATCH 67/82] Clean up TSVB type client code to conform to the schema (#68519) (#69885) * Clean up TSVB type client code to conform to the schema Part of #57342 * Replace FieldDescriptor with IFieldType, add UIRestrictions interface * Replace expect from chai with @kbn/expect, remove unnecessary type * Add TimeseriesUIRestrictions type and refactor add_delete_buttons.test * Replace some types with MetricsItemsSchema['values'] to avoid duplications Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../vis_type_timeseries/common/types.ts | 25 ++++ ...{ui_restrictions.js => ui_restrictions.ts} | 19 ++- .../vis_schema.ts} | 120 +++++++++--------- ...ns.test.js => add_delete_buttons.test.tsx} | 24 ++-- ...lete_buttons.js => add_delete_buttons.tsx} | 40 +++--- .../components/aggs/{agg.js => agg.tsx} | 40 +++--- .../aggs/{agg_row.js => agg_row.tsx} | 28 ++-- .../aggs/{agg_select.js => agg_select.tsx} | 52 ++++---- .../components/aggs/{aggs.js => aggs.tsx} | 26 ++-- ...multi_value_row.js => multi_value_row.tsx} | 37 ++++-- ...percentile_rank.js => percentile_rank.tsx} | 77 +++++------ ...k_values.js => percentile_rank_values.tsx} | 49 ++++--- ...d_agg.js => temporary_unsupported_agg.tsx} | 15 ++- ...unsupported_agg.js => unsupported_agg.tsx} | 15 ++- ..._metric_agg_fn.js => new_metric_agg_fn.ts} | 3 +- ...rag_handler.js => series_drag_handler.tsx} | 22 ++-- .../vis_type_timeseries/public/types.ts | 29 +++++ .../vis_type_timeseries/server/routes/vis.ts | 2 +- 18 files changed, 369 insertions(+), 254 deletions(-) create mode 100644 src/plugins/vis_type_timeseries/common/types.ts rename src/plugins/vis_type_timeseries/common/{ui_restrictions.js => ui_restrictions.ts} (73%) rename src/plugins/vis_type_timeseries/{server/routes/post_vis_schema.ts => common/vis_schema.ts} (73%) rename src/plugins/vis_type_timeseries/public/application/components/{add_delete_buttons.test.js => add_delete_buttons.test.tsx} (77%) rename src/plugins/vis_type_timeseries/public/application/components/{add_delete_buttons.js => add_delete_buttons.tsx} (87%) rename src/plugins/vis_type_timeseries/public/application/components/aggs/{agg.js => agg.tsx} (70%) rename src/plugins/vis_type_timeseries/public/application/components/aggs/{agg_row.js => agg_row.tsx} (86%) rename src/plugins/vis_type_timeseries/public/application/components/aggs/{agg_select.js => agg_select.tsx} (88%) rename src/plugins/vis_type_timeseries/public/application/components/aggs/{aggs.js => aggs.tsx} (83%) rename src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/{multi_value_row.js => multi_value_row.tsx} (79%) rename src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/{percentile_rank.js => percentile_rank.tsx} (75%) rename src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/{percentile_rank_values.js => percentile_rank_values.tsx} (67%) rename src/plugins/vis_type_timeseries/public/application/components/aggs/{temporary_unsupported_agg.js => temporary_unsupported_agg.tsx} (79%) rename src/plugins/vis_type_timeseries/public/application/components/aggs/{unsupported_agg.js => unsupported_agg.tsx} (80%) rename src/plugins/vis_type_timeseries/public/application/components/lib/{new_metric_agg_fn.js => new_metric_agg_fn.ts} (87%) rename src/plugins/vis_type_timeseries/public/application/components/{series_drag_handler.js => series_drag_handler.tsx} (85%) create mode 100644 src/plugins/vis_type_timeseries/public/types.ts diff --git a/src/plugins/vis_type_timeseries/common/types.ts b/src/plugins/vis_type_timeseries/common/types.ts new file mode 100644 index 0000000000000..4520069244527 --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/types.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { metricsItems, panel, seriesItems } from './vis_schema'; + +export type SeriesItemsSchema = TypeOf; +export type MetricsItemsSchema = TypeOf; +export type PanelSchema = TypeOf; diff --git a/src/plugins/vis_type_timeseries/common/ui_restrictions.js b/src/plugins/vis_type_timeseries/common/ui_restrictions.ts similarity index 73% rename from src/plugins/vis_type_timeseries/common/ui_restrictions.js rename to src/plugins/vis_type_timeseries/common/ui_restrictions.ts index 96726d51e4a7c..4508735f39ff9 100644 --- a/src/plugins/vis_type_timeseries/common/ui_restrictions.js +++ b/src/plugins/vis_type_timeseries/common/ui_restrictions.ts @@ -22,21 +22,30 @@ * @constant * @public */ -export const RESTRICTIONS_KEYS = { +export enum RESTRICTIONS_KEYS { /** * Key for getting the white listed group by fields from the UIRestrictions object. */ - WHITE_LISTED_GROUP_BY_FIELDS: 'whiteListedGroupByFields', + WHITE_LISTED_GROUP_BY_FIELDS = 'whiteListedGroupByFields', /** * Key for getting the white listed metrics from the UIRestrictions object. */ - WHITE_LISTED_METRICS: 'whiteListedMetrics', + WHITE_LISTED_METRICS = 'whiteListedMetrics', /** * Key for getting the white listed Time Range modes from the UIRestrictions object. */ - WHITE_LISTED_TIMERANGE_MODES: 'whiteListedTimerangeModes', + WHITE_LISTED_TIMERANGE_MODES = 'whiteListedTimerangeModes', +} + +export interface UIRestrictions { + '*': boolean; + [restriction: string]: boolean; +} + +export type TimeseriesUIRestrictions = { + [key in RESTRICTIONS_KEYS]: Record; }; /** @@ -44,6 +53,6 @@ export const RESTRICTIONS_KEYS = { * @constant * @public */ -export const DEFAULT_UI_RESTRICTION = { +export const DEFAULT_UI_RESTRICTION: UIRestrictions = { '*': true, }; diff --git a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts similarity index 73% rename from src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts rename to src/plugins/vis_type_timeseries/common/vis_schema.ts index bf2ea8651c5a2..7161c197b6940 100644 --- a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -76,7 +76,7 @@ const gaugeColorRulesItems = schema.object({ operator: stringOptionalNullable, value: schema.maybe(schema.nullable(schema.number())), }); -const metricsItems = schema.object({ +export const metricsItems = schema.object({ field: stringOptionalNullable, id: stringRequired, metric_agg: stringOptionalNullable, @@ -133,7 +133,7 @@ const splitFiltersItems = schema.object({ label: stringOptionalNullable, }); -const seriesItems = schema.object({ +export const seriesItems = schema.object({ aggregate_by: stringOptionalNullable, aggregate_function: stringOptionalNullable, axis_position: stringRequired, @@ -195,66 +195,66 @@ const seriesItems = schema.object({ var_name: stringOptionalNullable, }); +export const panel = schema.object({ + annotations: schema.maybe(schema.arrayOf(annotationsItems)), + axis_formatter: stringRequired, + axis_position: stringRequired, + axis_scale: stringRequired, + axis_min: stringOrNumberOptionalNullable, + axis_max: stringOrNumberOptionalNullable, + bar_color_rules: schema.maybe(arrayNullable), + background_color: stringOptionalNullable, + background_color_rules: schema.maybe(schema.arrayOf(backgroundColorRulesItems)), + default_index_pattern: stringOptionalNullable, + default_timefield: stringOptionalNullable, + drilldown_url: stringOptionalNullable, + drop_last_bucket: numberIntegerOptional, + filter: schema.nullable( + schema.oneOf([ + stringOptionalNullable, + schema.object({ + language: stringOptionalNullable, + query: stringOptionalNullable, + }), + ]) + ), + gauge_color_rules: schema.maybe(schema.arrayOf(gaugeColorRulesItems)), + gauge_width: schema.nullable(schema.oneOf([stringOptionalNullable, numberOptional])), + gauge_inner_color: stringOptionalNullable, + gauge_inner_width: stringOrNumberOptionalNullable, + gauge_style: stringOptionalNullable, + gauge_max: stringOrNumberOptionalNullable, + id: stringRequired, + ignore_global_filters: numberOptional, + ignore_global_filter: numberOptional, + index_pattern: stringRequired, + interval: stringRequired, + isModelInvalid: schema.maybe(schema.boolean()), + legend_position: stringOptionalNullable, + markdown: stringOptionalNullable, + markdown_scrollbars: numberIntegerOptional, + markdown_openLinksInNewTab: numberIntegerOptional, + markdown_vertical_align: stringOptionalNullable, + markdown_less: stringOptionalNullable, + markdown_css: stringOptionalNullable, + pivot_id: stringOptionalNullable, + pivot_label: stringOptionalNullable, + pivot_type: stringOptionalNullable, + pivot_rows: stringOptionalNullable, + series: schema.arrayOf(seriesItems), + show_grid: numberIntegerRequired, + show_legend: numberIntegerRequired, + tooltip_mode: schema.maybe( + schema.oneOf([schema.literal('show_all'), schema.literal('show_focused')]) + ), + time_field: stringOptionalNullable, + time_range_mode: stringOptionalNullable, + type: stringRequired, +}); + export const visPayloadSchema = schema.object({ filters: arrayNullable, - panels: schema.arrayOf( - schema.object({ - annotations: schema.maybe(schema.arrayOf(annotationsItems)), - axis_formatter: stringRequired, - axis_position: stringRequired, - axis_scale: stringRequired, - axis_min: stringOrNumberOptionalNullable, - axis_max: stringOrNumberOptionalNullable, - bar_color_rules: schema.maybe(arrayNullable), - background_color: stringOptionalNullable, - background_color_rules: schema.maybe(schema.arrayOf(backgroundColorRulesItems)), - default_index_pattern: stringOptionalNullable, - default_timefield: stringOptionalNullable, - drilldown_url: stringOptionalNullable, - drop_last_bucket: numberIntegerOptional, - filter: schema.nullable( - schema.oneOf([ - stringOptionalNullable, - schema.object({ - language: stringOptionalNullable, - query: stringOptionalNullable, - }), - ]) - ), - gauge_color_rules: schema.maybe(schema.arrayOf(gaugeColorRulesItems)), - gauge_width: schema.nullable(schema.oneOf([stringOptionalNullable, numberOptional])), - gauge_inner_color: stringOptionalNullable, - gauge_inner_width: stringOrNumberOptionalNullable, - gauge_style: stringOptionalNullable, - gauge_max: stringOrNumberOptionalNullable, - id: stringRequired, - ignore_global_filters: numberOptional, - ignore_global_filter: numberOptional, - index_pattern: stringRequired, - interval: stringRequired, - isModelInvalid: schema.maybe(schema.boolean()), - legend_position: stringOptionalNullable, - markdown: stringOptionalNullable, - markdown_scrollbars: numberIntegerOptional, - markdown_openLinksInNewTab: numberIntegerOptional, - markdown_vertical_align: stringOptionalNullable, - markdown_less: stringOptionalNullable, - markdown_css: stringOptionalNullable, - pivot_id: stringOptionalNullable, - pivot_label: stringOptionalNullable, - pivot_type: stringOptionalNullable, - pivot_rows: stringOptionalNullable, - series: schema.arrayOf(seriesItems), - show_grid: numberIntegerRequired, - show_legend: numberIntegerRequired, - tooltip_mode: schema.maybe( - schema.oneOf([schema.literal('show_all'), schema.literal('show_focused')]) - ), - time_field: stringOptionalNullable, - time_range_mode: stringOptionalNullable, - type: stringRequired, - }) - ), + panels: schema.arrayOf(panel), // general query: schema.nullable(schema.arrayOf(queryObject)), state: schema.object({ diff --git a/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.js b/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.tsx similarity index 77% rename from src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.js rename to src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.tsx index 7afa71d6ba38f..0fb3e80344e2b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.test.tsx @@ -18,51 +18,49 @@ */ import React from 'react'; -import { expect } from 'chai'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import sinon from 'sinon'; import { AddDeleteButtons } from './add_delete_buttons'; describe('AddDeleteButtons', () => { it('calls onAdd={handleAdd}', () => { - const handleAdd = sinon.spy(); + const handleAdd = jest.fn(); const wrapper = shallowWithIntl(); wrapper.find('EuiButtonIcon').at(0).simulate('click'); - expect(handleAdd.calledOnce).to.equal(true); + expect(handleAdd).toHaveBeenCalled(); }); it('calls onDelete={handleDelete}', () => { - const handleDelete = sinon.spy(); + const handleDelete = jest.fn(); const wrapper = shallowWithIntl(); wrapper.find('EuiButtonIcon').at(1).simulate('click'); - expect(handleDelete.calledOnce).to.equal(true); + expect(handleDelete).toHaveBeenCalled(); }); it('calls onClone={handleClone}', () => { - const handleClone = sinon.spy(); + const handleClone = jest.fn(); const wrapper = shallowWithIntl(); wrapper.find('EuiButtonIcon').at(0).simulate('click'); - expect(handleClone.calledOnce).to.equal(true); + expect(handleClone).toHaveBeenCalled(); }); it('disableDelete={true}', () => { const wrapper = shallowWithIntl(); - expect(wrapper.find({ text: 'Delete' })).to.have.length(0); + expect(wrapper.find({ text: 'Delete' })).toHaveLength(0); }); it('disableAdd={true}', () => { const wrapper = shallowWithIntl(); - expect(wrapper.find({ text: 'Add' })).to.have.length(0); + expect(wrapper.find({ text: 'Add' })).toHaveLength(0); }); it('should not display clone by default', () => { const wrapper = shallowWithIntl(); - expect(wrapper.find({ text: 'Clone' })).to.have.length(0); + expect(wrapper.find({ text: 'Clone' })).toHaveLength(0); }); it('should not display clone when disableAdd={true}', () => { - const fn = sinon.spy(); + const fn = jest.fn(); const wrapper = shallowWithIntl(); - expect(wrapper.find({ text: 'Clone' })).to.have.length(0); + expect(wrapper.find({ text: 'Clone' })).toHaveLength(0); }); }); diff --git a/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.js b/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.tsx similarity index 87% rename from src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.js rename to src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.tsx index 798d16947c3d9..7502de1cb1aa4 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.js +++ b/src/plugins/vis_type_timeseries/public/application/components/add_delete_buttons.tsx @@ -17,13 +17,29 @@ * under the License. */ -import PropTypes from 'prop-types'; -import React from 'react'; -import { EuiToolTip, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { MouseEvent } from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isBoolean } from 'lodash'; -export function AddDeleteButtons(props) { +interface AddDeleteButtonsProps { + addTooltip: string; + deleteTooltip: string; + cloneTooltip: string; + activatePanelTooltip: string; + deactivatePanelTooltip: string; + isPanelActive?: boolean; + disableAdd?: boolean; + disableDelete?: boolean; + responsive?: boolean; + testSubj: string; + togglePanelActivation?: () => void; + onClone?: () => void; + onAdd?: () => void; + onDelete?: (event: MouseEvent) => void; +} + +export function AddDeleteButtons(props: AddDeleteButtonsProps) { const { testSubj } = props; const createDelete = () => { if (props.disableDelete) { @@ -147,19 +163,3 @@ AddDeleteButtons.defaultProps = { } ), }; - -AddDeleteButtons.propTypes = { - addTooltip: PropTypes.string, - deleteTooltip: PropTypes.string, - cloneTooltip: PropTypes.string, - activatePanelTooltip: PropTypes.string, - deactivatePanelTooltip: PropTypes.string, - togglePanelActivation: PropTypes.func, - isPanelActive: PropTypes.bool, - disableAdd: PropTypes.bool, - disableDelete: PropTypes.bool, - onClone: PropTypes.func, - onAdd: PropTypes.func, - onDelete: PropTypes.func, - responsive: PropTypes.bool, -}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx similarity index 70% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/agg.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx index d547f64f13f67..e5236c3833b19 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx @@ -17,15 +17,33 @@ * under the License. */ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { HTMLAttributes } from 'react'; +// @ts-ignore import { aggToComponent } from '../lib/agg_to_component'; +// @ts-ignore +import { isMetricEnabled } from '../../lib/check_ui_restrictions'; import { UnsupportedAgg } from './unsupported_agg'; import { TemporaryUnsupportedAgg } from './temporary_unsupported_agg'; +import { MetricsItemsSchema, PanelSchema, SeriesItemsSchema } from '../../../../common/types'; +import { DragHandleProps } from '../../../types'; +import { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions'; +import { IFieldType } from '../../../../../data/common/index_patterns/fields'; -import { isMetricEnabled } from '../../lib/check_ui_restrictions'; +interface AggProps extends HTMLAttributes { + disableDelete: boolean; + fields: IFieldType[]; + model: MetricsItemsSchema; + panel: PanelSchema; + series: SeriesItemsSchema; + siblings: MetricsItemsSchema[]; + uiRestrictions: TimeseriesUIRestrictions; + dragHandleProps: DragHandleProps; + onAdd: () => void; + onChange: () => void; + onDelete: () => void; +} -export function Agg(props) { +export function Agg(props: AggProps) { const { model, uiRestrictions } = props; let Component = aggToComponent[model.type]; @@ -59,17 +77,3 @@ export function Agg(props) {
); } - -Agg.propTypes = { - disableDelete: PropTypes.bool, - fields: PropTypes.object, - model: PropTypes.object, - onAdd: PropTypes.func, - onChange: PropTypes.func, - onDelete: PropTypes.func, - panel: PropTypes.object, - series: PropTypes.object, - siblings: PropTypes.array, - uiRestrictions: PropTypes.object, - dragHandleProps: PropTypes.object, -}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx similarity index 86% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx index a2f1640904dd0..0363ba486a775 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_row.tsx @@ -17,15 +17,26 @@ * under the License. */ -import PropTypes from 'prop-types'; import React from 'react'; import { last } from 'lodash'; -import { AddDeleteButtons } from '../add_delete_buttons'; import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { SeriesDragHandler } from '../series_drag_handler'; import { i18n } from '@kbn/i18n'; +import { AddDeleteButtons } from '../add_delete_buttons'; +import { SeriesDragHandler } from '../series_drag_handler'; +import { MetricsItemsSchema } from '../../../../common/types'; +import { DragHandleProps } from '../../../types'; -export function AggRow(props) { +interface AggRowProps { + disableDelete: boolean; + model: MetricsItemsSchema; + siblings: MetricsItemsSchema[]; + dragHandleProps: DragHandleProps; + children: React.ReactNode; + onAdd: () => void; + onDelete: () => void; +} + +export function AggRow(props: AggRowProps) { let iconType = 'eyeClosed'; let iconColor = 'subdued'; const lastSibling = last(props.siblings); @@ -71,12 +82,3 @@ export function AggRow(props) {
); } - -AggRow.propTypes = { - disableDelete: PropTypes.bool, - model: PropTypes.object, - onAdd: PropTypes.func, - onDelete: PropTypes.func, - siblings: PropTypes.array, - dragHandleProps: PropTypes.object, -}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx similarity index 88% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx index 7ff6b6eb56692..6fa1a2adaa08e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx @@ -17,14 +17,17 @@ * under the License. */ -import PropTypes from 'prop-types'; import React from 'react'; -import { EuiComboBox } from '@elastic/eui'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { injectI18n } from '@kbn/i18n/react'; +// @ts-ignore import { isMetricEnabled } from '../../lib/check_ui_restrictions'; +import { MetricsItemsSchema } from '../../../../common/types'; +import { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions'; -const metricAggs = [ +type AggSelectOption = EuiComboBoxOptionOption; + +const metricAggs: AggSelectOption[] = [ { label: i18n.translate('visTypeTimeseries.aggSelect.metricsAggs.averageLabel', { defaultMessage: 'Average', @@ -123,7 +126,7 @@ const metricAggs = [ }, ]; -const pipelineAggs = [ +const pipelineAggs: AggSelectOption[] = [ { label: i18n.translate('visTypeTimeseries.aggSelect.pipelineAggs.bucketScriptLabel', { defaultMessage: 'Bucket Script', @@ -162,7 +165,7 @@ const pipelineAggs = [ }, ]; -const siblingAggs = [ +const siblingAggs: AggSelectOption[] = [ { label: i18n.translate('visTypeTimeseries.aggSelect.siblingAggs.overallAverageLabel', { defaultMessage: 'Overall Average', @@ -207,7 +210,7 @@ const siblingAggs = [ }, ]; -const specialAggs = [ +const specialAggs: AggSelectOption[] = [ { label: i18n.translate('visTypeTimeseries.aggSelect.specialAggs.seriesAggLabel', { defaultMessage: 'Series Agg', @@ -224,14 +227,23 @@ const specialAggs = [ const allAggOptions = [...metricAggs, ...pipelineAggs, ...siblingAggs, ...specialAggs]; -function filterByPanelType(panelType) { - return (agg) => { +function filterByPanelType(panelType: string) { + return (agg: AggSelectOption) => { if (panelType === 'table') return agg.value !== 'series_agg'; return true; }; } -function AggSelectUi(props) { +interface AggSelectUiProps { + id: string; + panelType: string; + siblings: MetricsItemsSchema[]; + value: string; + uiRestrictions?: TimeseriesUIRestrictions; + onChange: (currentlySelectedOptions: AggSelectOption[]) => void; +} + +export function AggSelect(props: AggSelectUiProps) { const { siblings, panelType, value, onChange, uiRestrictions, ...rest } = props; const selectedOptions = allAggOptions.filter((option) => { @@ -242,11 +254,11 @@ function AggSelectUi(props) { if (siblings.length <= 1) enablePipelines = false; - let options; + let options: EuiComboBoxOptionOption[]; if (panelType === 'metrics') { options = metricAggs; } else { - const disableSiblingAggs = (agg) => ({ + const disableSiblingAggs = (agg: AggSelectOption) => ({ ...agg, disabled: !enablePipelines || !isMetricEnabled(agg.value, uiRestrictions), }); @@ -282,9 +294,9 @@ function AggSelectUi(props) { ]; } - const handleChange = (selectedOptions) => { - if (!selectedOptions || selectedOptions.length <= 0) return; - onChange(selectedOptions); + const handleChange = (currentlySelectedOptions: AggSelectOption[]) => { + if (!currentlySelectedOptions || currentlySelectedOptions.length <= 0) return; + onChange(currentlySelectedOptions); }; return ( @@ -303,13 +315,3 @@ function AggSelectUi(props) {
); } - -AggSelectUi.propTypes = { - onChange: PropTypes.func, - panelType: PropTypes.string, - siblings: PropTypes.array, - value: PropTypes.string, - uiRestrictions: PropTypes.object, -}; - -export const AggSelect = injectI18n(AggSelectUi); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.tsx similarity index 83% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.tsx index 772b62b14f811..af3e42a59612b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.tsx @@ -18,18 +18,29 @@ */ import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; import { EuiDraggable, EuiDroppable } from '@elastic/eui'; import { Agg } from './agg'; -import { newMetricAggFn } from '../lib/new_metric_agg_fn'; +// @ts-ignore import { seriesChangeHandler } from '../lib/series_change_handler'; +// @ts-ignore import { handleAdd, handleDelete } from '../lib/collection_actions'; +import { newMetricAggFn } from '../lib/new_metric_agg_fn'; +import { PanelSchema, SeriesItemsSchema } from '../../../../common/types'; +import { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions'; +import { IFieldType } from '../../../../../data/common/index_patterns/fields'; const DROPPABLE_ID = 'aggs_dnd'; -export class Aggs extends PureComponent { +export interface AggsProps { + panel: PanelSchema; + model: SeriesItemsSchema; + fields: IFieldType[]; + uiRestrictions: TimeseriesUIRestrictions; +} + +export class Aggs extends PureComponent { render() { const { panel, model, fields, uiRestrictions } = this.props; const list = model.metrics; @@ -68,12 +79,3 @@ export class Aggs extends PureComponent { ); } } - -Aggs.propTypes = { - name: PropTypes.string.isRequired, - fields: PropTypes.object.isRequired, - model: PropTypes.object.isRequired, - onChange: PropTypes.func.isRequired, - panel: PropTypes.object.isRequired, - dragHandleProps: PropTypes.object, -}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx similarity index 79% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx index fd64559cc1ec2..ef8876a19b1a6 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { ChangeEvent } from 'react'; import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -31,10 +30,29 @@ import { import { AddDeleteButtons } from '../../add_delete_buttons'; -export const MultiValueRow = ({ model, onChange, onDelete, onAdd, disableAdd, disableDelete }) => { +interface MultiValueRowProps { + model: { + id: number; + value: string; + }; + disableAdd: boolean; + disableDelete: boolean; + onChange: ({ value, id }: { id: number; value: string }) => void; + onDelete: (model: { id: number; value: string }) => void; + onAdd: () => void; +} + +export const MultiValueRow = ({ + model, + onChange, + onDelete, + onAdd, + disableAdd, + disableDelete, +}: MultiValueRowProps) => { const htmlId = htmlIdGenerator(); - const onFieldNumberChange = (event) => + const onFieldNumberChange = (event: ChangeEvent) => onChange({ ...model, value: get(event, 'target.value'), @@ -54,7 +72,7 @@ export const MultiValueRow = ({ model, onChange, onDelete, onAdd, disableAdd, di @@ -78,12 +96,3 @@ MultiValueRow.defaultProps = { value: '', }, }; - -MultiValueRow.propTypes = { - model: PropTypes.object, - onChange: PropTypes.func, - onDelete: PropTypes.func, - onAdd: PropTypes.func, - defaultAddValue: PropTypes.string, - disableDelete: PropTypes.bool, -}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx similarity index 75% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx index c8af4089ed783..a16f5aeefc49c 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx @@ -17,16 +17,7 @@ * under the License. */ -import PropTypes from 'prop-types'; import React from 'react'; -import { assign } from 'lodash'; -import { AggSelect } from '../agg_select'; -import { FieldSelect } from '../field_select'; -import { AggRow } from '../agg_row'; -import { createChangeHandler } from '../../lib/create_change_handler'; -import { createSelectHandler } from '../../lib/create_select_handler'; -import { PercentileRankValues } from './percentile_rank_values'; - import { htmlIdGenerator, EuiFlexGroup, @@ -36,11 +27,36 @@ import { EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +import { AggSelect } from '../agg_select'; +// @ts-ignore +import { FieldSelect } from '../field_select'; +// @ts-ignore +import { createChangeHandler } from '../../lib/create_change_handler'; +// @ts-ignore +import { createSelectHandler } from '../../lib/create_select_handler'; +import { AggRow } from '../agg_row'; +import { PercentileRankValues } from './percentile_rank_values'; + +import { IFieldType, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; +import { MetricsItemsSchema, PanelSchema, SeriesItemsSchema } from '../../../../../common/types'; +import { DragHandleProps } from '../../../../types'; const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER]; -export const PercentileRankAgg = (props) => { +interface PercentileRankAggProps { + disableDelete: boolean; + fields: IFieldType[]; + model: MetricsItemsSchema; + panel: PanelSchema; + series: SeriesItemsSchema; + siblings: MetricsItemsSchema[]; + dragHandleProps: DragHandleProps; + onAdd(): void; + onChange(): void; + onDelete(): void; +} + +export const PercentileRankAgg = (props: PercentileRankAggProps) => { const { series, panel, fields } = props; const defaults = { values: [''] }; const model = { ...defaults, ...props.model }; @@ -52,12 +68,11 @@ export const PercentileRankAgg = (props) => { const handleChange = createChangeHandler(props.onChange, model); const handleSelectChange = createSelectHandler(handleChange); - const handlePercentileRankValuesChange = (values) => { - handleChange( - assign({}, model, { - values, - }) - ); + const handlePercentileRankValuesChange = (values: MetricsItemsSchema['values']) => { + handleChange({ + ...model, + values, + }); }; return ( @@ -108,25 +123,15 @@ export const PercentileRankAgg = (props) => { - + {model.values && ( + + )} ); }; - -PercentileRankAgg.propTypes = { - disableDelete: PropTypes.bool, - fields: PropTypes.object, - model: PropTypes.object, - onAdd: PropTypes.func, - onChange: PropTypes.func, - onDelete: PropTypes.func, - panel: PropTypes.object, - series: PropTypes.object, - siblings: PropTypes.array, -}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx similarity index 67% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx index 6d52eb9e3515c..b66d79d67f427 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx @@ -16,34 +16,49 @@ * specific language governing permissions and limitations * under the License. */ -import PropTypes from 'prop-types'; import React from 'react'; import { last } from 'lodash'; import { EuiFlexGroup } from '@elastic/eui'; import { MultiValueRow } from './multi_value_row'; -export const PercentileRankValues = (props) => { +interface PercentileRankValuesProps { + model: Array; + disableDelete: boolean; + disableAdd: boolean; + showOnlyLastRow: boolean; + onChange: (values: any[]) => void; +} + +export const PercentileRankValues = (props: PercentileRankValuesProps) => { const model = props.model || []; const { onChange, disableAdd, disableDelete, showOnlyLastRow } = props; - const onChangeValue = ({ value, id }) => { + const onChangeValue = ({ value, id }: { value: string; id: number }) => { model[id] = value; onChange(model); }; - const onDeleteValue = ({ id }) => + const onDeleteValue = ({ id }: { id: number }) => onChange(model.filter((item, currentIndex) => id !== currentIndex)); const onAddValue = () => onChange([...model, '']); - const renderRow = ({ rowModel, disableDelete, disableAdd }) => ( + const renderRow = ({ + rowModel, + disableDeleteRow, + disableAddRow, + }: { + rowModel: { id: number; value: string }; + disableDeleteRow: boolean; + disableAddRow: boolean; + }) => ( ); @@ -54,10 +69,10 @@ export const PercentileRankValues = (props) => { renderRow({ rowModel: { id: model.length - 1, - value: last(model), + value: last(model) || '', }, - disableAdd: true, - disableDelete: true, + disableAddRow: true, + disableDeleteRow: true, })} {!showOnlyLastRow && @@ -65,20 +80,12 @@ export const PercentileRankValues = (props) => { renderRow({ rowModel: { id, - value, + value: value || '', }, - disableAdd, - disableDelete: disableDelete || array.length < 2, + disableAddRow: disableAdd, + disableDeleteRow: disableDelete || array.length < 2, }) )} ); }; - -PercentileRankValues.propTypes = { - model: PropTypes.array, - onChange: PropTypes.func, - disableDelete: PropTypes.bool, - disableAdd: PropTypes.bool, - showOnlyLastRow: PropTypes.bool, -}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.tsx similarity index 79% rename from src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.js rename to src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.tsx index bae0491d978a2..d10c7ea7a7e36 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/temporary_unsupported_agg.tsx @@ -17,12 +17,23 @@ * under the License. */ -import { AggRow } from './agg_row'; import React from 'react'; import { EuiCode, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AggRow } from './agg_row'; +import { MetricsItemsSchema } from '../../../../common/types'; +import { DragHandleProps } from '../../../types'; + +interface TemporaryUnsupportedAggProps { + disableDelete: boolean; + model: MetricsItemsSchema; + siblings: MetricsItemsSchema[]; + dragHandleProps: DragHandleProps; + onAdd: () => void; + onDelete: () => void; +} -export function TemporaryUnsupportedAgg(props) { +export function TemporaryUnsupportedAgg(props: TemporaryUnsupportedAggProps) { return ( void; + onDelete: () => void; +} -export function UnsupportedAgg(props) { +export function UnsupportedAgg(props: UnsupportedAggProps) { return ( { +export const newMetricAggFn = (): MetricsItemsSchema => { return { id: uuid.v1(), type: 'count', diff --git a/src/plugins/vis_type_timeseries/public/application/components/series_drag_handler.js b/src/plugins/vis_type_timeseries/public/application/components/series_drag_handler.tsx similarity index 85% rename from src/plugins/vis_type_timeseries/public/application/components/series_drag_handler.js rename to src/plugins/vis_type_timeseries/public/application/components/series_drag_handler.tsx index f978348a5e45c..73293a0d330fd 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/series_drag_handler.js +++ b/src/plugins/vis_type_timeseries/public/application/components/series_drag_handler.tsx @@ -18,11 +18,20 @@ */ import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; import { EuiFlexItem, EuiToolTip, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { DragHandleProps } from '../../types'; + +interface SeriesDragHandlerProps { + hideDragHandler: boolean; + dragHandleProps: DragHandleProps; +} + +export class SeriesDragHandler extends PureComponent { + static defaultProps = { + hideDragHandler: true, + }; -export class SeriesDragHandler extends PureComponent { render() { const { dragHandleProps, hideDragHandler } = this.props; @@ -49,12 +58,3 @@ export class SeriesDragHandler extends PureComponent { ); } } - -SeriesDragHandler.defaultProps = { - hideDragHandler: true, -}; - -SeriesDragHandler.propTypes = { - hideDragHandler: PropTypes.bool, - dragHandleProps: PropTypes.object.isRequired, -}; diff --git a/src/plugins/vis_type_timeseries/public/types.ts b/src/plugins/vis_type_timeseries/public/types.ts new file mode 100644 index 0000000000000..338118dcdc5aa --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/types.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiDraggable } from '@elastic/eui'; + +type PropsOf = T extends React.ComponentType ? ComponentProps : never; +type FirstArgumentOf = Func extends (arg1: infer FirstArgument, ...rest: any[]) => any + ? FirstArgument + : never; +export type DragHandleProps = FirstArgumentOf< + Exclude['children'], React.ReactElement> +>['dragHandleProps']; diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index 744020b583882..48efd4398e4d4 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -20,7 +20,7 @@ import { IRouter, KibanaRequest } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { getVisData, GetVisDataOptions } from '../lib/get_vis_data'; -import { visPayloadSchema } from './post_vis_schema'; +import { visPayloadSchema } from '../../common/vis_schema'; import { Framework, ValidationTelemetryServiceSetup } from '../index'; const escapeHatch = schema.object({}, { unknowns: 'allow' }); From 6a9bcc37dc22ecaee1befdeb9fa463f682da1810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Fri, 26 Jun 2020 09:02:56 +0200 Subject: [PATCH 68/82] [7.x][DOCS] Fixes wording in Upload a CSV section (#70040) * [7.x][DOCS] Fixes wording in Upload a CSV section. * Apply suggestions from code review * Apply suggestions from code review --- docs/setup/connect-to-elasticsearch.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 0575b8532508f..bffb3f97cd1b9 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -23,8 +23,8 @@ experimental[] To visualize data in a CSV, JSON, or log file, you can upload it using the File Data Visualizer. On the home page, click *Import a CSV, NDSON, or log file*, and then drag your file into the File Data Visualizer. Alternatively, you can open -it by navigating to the Machine Learning app page from the sidebar menu and -selecting the Data Visualizer from the top navigation bar on the opening page. +it by navigating to *Machine Learning* from the side navigation and selecting +*Data Visualizer*. [role="screenshot"] image::images/data-viz-homepage.jpg[File Data Visualizer on the home page] From cdd69144e42bc208a3380c00dad6bbceef598160 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 26 Jun 2020 10:57:12 +0200 Subject: [PATCH 69/82] [Discover] Validate timerange before submitting query to ES (#69363) (#70037) --- .../public/application/angular/discover.js | 15 ++++-- .../helpers/validate_time_range.test.ts | 47 ++++++++++++++++++ .../helpers/validate_time_range.ts | 49 +++++++++++++++++++ test/functional/apps/discover/_discover.js | 11 +++++ test/functional/page_objects/common_page.ts | 2 +- 5 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 src/plugins/discover/public/application/helpers/validate_time_range.test.ts create mode 100644 src/plugins/discover/public/application/helpers/validate_time_range.ts diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 65868b0b7cd46..f7f88603b8332 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -64,6 +64,7 @@ const { } = getServices(); import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; +import { validateTimeRange } from '../helpers/validate_time_range'; import { esFilters, indexPatterns as indexPatternsUtils, @@ -784,6 +785,10 @@ function discoverController( if (!init.complete) return; $scope.fetchCounter++; $scope.fetchError = undefined; + if (!validateTimeRange(timefilter.getTime(), toastNotifications)) { + $scope.resultState = 'none'; + return; + } // Abort any in-progress requests before fetching again if (abortController) abortController.abort(); @@ -916,14 +921,18 @@ function discoverController( } $scope.updateTime = function () { - //this is the timerange for the histogram, should be refactored + const { from, to } = timefilter.getTime(); + // this is the timerange for the histogram, should be refactored $scope.timeRange = { - from: dateMath.parse(timefilter.getTime().from), - to: dateMath.parse(timefilter.getTime().to, { roundUp: true }), + from: dateMath.parse(from), + to: dateMath.parse(to, { roundUp: true }), }; }; $scope.toMoment = function (datetime) { + if (!datetime) { + return; + } return moment(datetime).format(config.get('dateFormat')); }; diff --git a/src/plugins/discover/public/application/helpers/validate_time_range.test.ts b/src/plugins/discover/public/application/helpers/validate_time_range.test.ts new file mode 100644 index 0000000000000..a61a729caa22b --- /dev/null +++ b/src/plugins/discover/public/application/helpers/validate_time_range.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { validateTimeRange } from './validate_time_range'; +import { notificationServiceMock } from '../../../../../core/public/mocks'; + +describe('Discover validateTimeRange', () => { + test('validates given time ranges correctly', async () => { + const { toasts } = notificationServiceMock.createStartContract(); + [ + { from: '', to: '', result: false }, + { from: 'now', to: 'now+1h', result: true }, + { from: 'now', to: 'lala+1h', result: false }, + { from: '', to: 'now', result: false }, + { from: 'now', to: '', result: false }, + { from: ' 2020-06-02T13:36:13.689Z', to: 'now', result: true }, + { from: ' 2020-06-02T13:36:13.689Z', to: '2020-06-02T13:36:13.690Z', result: true }, + ].map((test) => { + expect(validateTimeRange({ from: test.from, to: test.to }, toasts)).toEqual(test.result); + }); + }); + + test('displays a toast when invalid data is entered', async () => { + const { toasts } = notificationServiceMock.createStartContract(); + expect(validateTimeRange({ from: 'now', to: 'null' }, toasts)).toEqual(false); + expect(toasts.addDanger).toHaveBeenCalledWith({ + title: 'Invalid time range', + text: "The provided time range is invalid. (from: 'now', to: 'null')", + }); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/validate_time_range.ts b/src/plugins/discover/public/application/helpers/validate_time_range.ts new file mode 100644 index 0000000000000..411147f827333 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/validate_time_range.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import dateMath from '@elastic/datemath'; +import { i18n } from '@kbn/i18n'; +import { ToastsStart } from 'kibana/public'; + +/** + * Validates a given time filter range, provided by URL or UI + * Unless valid, it returns false and displays a notification + */ +export function validateTimeRange( + { from, to }: { from: string; to: string }, + toastNotifications: ToastsStart +): boolean { + const fromMoment = dateMath.parse(from); + const toMoment = dateMath.parse(to); + if (!fromMoment || !toMoment || !fromMoment.isValid() || !toMoment.isValid()) { + toastNotifications.addDanger({ + title: i18n.translate('discover.notifications.invalidTimeRangeTitle', { + defaultMessage: `Invalid time range`, + }), + text: i18n.translate('discover.notifications.invalidTimeRangeText', { + defaultMessage: `The provided time range is invalid. (from: '{from}', to: '{to}')`, + values: { + from, + to, + }, + }), + }); + return false; + } + return true; +} diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index db292207d76c3..ef0b2961557b0 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -282,5 +282,16 @@ export default function ({ getService, getPageObjects }) { expect(refreshedTimeString).not.to.be(initialTimeString); }); }); + + describe('invalid time range in URL', function () { + it('should display a "Invalid time range toast"', async function () { + await PageObjects.common.navigateToUrl('discover', '#/?_g=(time:(from:now-15m,to:null))', { + useActualUrl: true, + }); + await PageObjects.header.awaitKibanaChrome(); + const toastMessage = await PageObjects.common.closeToast(); + expect(toastMessage).to.be('Invalid time range'); + }); + }); }); } diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 20d4721d3001e..efff6dbc8a8f9 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -393,7 +393,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo const toast = await find.byCssSelector('.euiToast', 2 * defaultFindTimeout); await toast.moveMouseTo(); const title = await (await find.byCssSelector('.euiToastHeader__title')).getVisibleText(); - log.debug(`Toast title: ${title}`); + await find.clickByCssSelector('.euiToast__closeButton'); return title; } From 5dde009c63207f4ee95b9165c0732cfe87645e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 26 Jun 2020 10:11:14 +0100 Subject: [PATCH 70/82] [Observability] Fixing dynamic return type based on the appName (#69894) (#69960) * fixing generic return type * addressing pr comments --- .../observability/public/data_handler.test.ts | 365 ++++++++++++++++++ .../observability/public/data_handler.ts | 26 +- x-pack/plugins/observability/public/index.ts | 6 +- x-pack/plugins/observability/public/plugin.ts | 8 +- 4 files changed, 385 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/observability/public/data_handler.test.ts diff --git a/x-pack/plugins/observability/public/data_handler.test.ts b/x-pack/plugins/observability/public/data_handler.test.ts new file mode 100644 index 0000000000000..71c2c942239fd --- /dev/null +++ b/x-pack/plugins/observability/public/data_handler.test.ts @@ -0,0 +1,365 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { registerDataHandler, getDataHandler } from './data_handler'; + +const params = { + startTime: '0', + endTime: '1', + bucketSize: '10s', +}; + +describe('registerDataHandler', () => { + describe('APM', () => { + registerDataHandler({ + appName: 'apm', + fetchData: async () => { + return { + title: 'apm', + appLink: '/apm', + stats: { + services: { + label: 'services', + type: 'number', + value: 1, + }, + transactions: { + label: 'transactions', + type: 'number', + value: 1, + }, + }, + series: { + transactions: { + label: 'transactions', + coordinates: [{ x: 1 }], + }, + }, + }; + }, + hasData: async () => true, + }); + + it('registered data handler', () => { + const dataHandler = getDataHandler('apm'); + expect(dataHandler?.fetchData).toBeDefined(); + expect(dataHandler?.hasData).toBeDefined(); + }); + + it('returns data when fetchData is called', async () => { + const dataHandler = getDataHandler('apm'); + const response = await dataHandler?.fetchData(params); + expect(response).toEqual({ + title: 'apm', + appLink: '/apm', + stats: { + services: { + label: 'services', + type: 'number', + value: 1, + }, + transactions: { + label: 'transactions', + type: 'number', + value: 1, + }, + }, + series: { + transactions: { + label: 'transactions', + coordinates: [{ x: 1 }], + }, + }, + }); + }); + + it('returns true when hasData is called', async () => { + const dataHandler = getDataHandler('apm'); + const hasData = await dataHandler?.hasData(); + expect(hasData).toBeTruthy(); + }); + }); + describe('Logs', () => { + registerDataHandler({ + appName: 'infra_logs', + fetchData: async () => { + return { + title: 'logs', + appLink: '/logs', + stats: { + foo: { + label: 'Foo', + type: 'number', + value: 1, + }, + bar: { + label: 'bar', + type: 'number', + value: 1, + }, + }, + series: { + foo: { + label: 'Foo', + coordinates: [{ x: 1 }], + }, + bar: { + label: 'Bar', + coordinates: [{ x: 1 }], + }, + }, + }; + }, + hasData: async () => true, + }); + + it('registered data handler', () => { + const dataHandler = getDataHandler('infra_logs'); + expect(dataHandler?.fetchData).toBeDefined(); + expect(dataHandler?.hasData).toBeDefined(); + }); + + it('returns data when fetchData is called', async () => { + const dataHandler = getDataHandler('infra_logs'); + const response = await dataHandler?.fetchData(params); + expect(response).toEqual({ + title: 'logs', + appLink: '/logs', + stats: { + foo: { + label: 'Foo', + type: 'number', + value: 1, + }, + bar: { + label: 'bar', + type: 'number', + value: 1, + }, + }, + series: { + foo: { + label: 'Foo', + coordinates: [{ x: 1 }], + }, + bar: { + label: 'Bar', + coordinates: [{ x: 1 }], + }, + }, + }); + }); + + it('returns true when hasData is called', async () => { + const dataHandler = getDataHandler('apm'); + const hasData = await dataHandler?.hasData(); + expect(hasData).toBeTruthy(); + }); + }); + describe('Uptime', () => { + registerDataHandler({ + appName: 'uptime', + fetchData: async () => { + return { + title: 'uptime', + appLink: '/uptime', + stats: { + monitors: { + label: 'Monitors', + type: 'number', + value: 1, + }, + up: { + label: 'Up', + type: 'number', + value: 1, + }, + down: { + label: 'Down', + type: 'number', + value: 1, + }, + }, + series: { + down: { + label: 'Down', + coordinates: [{ x: 1 }], + }, + up: { + label: 'Up', + coordinates: [{ x: 1 }], + }, + }, + }; + }, + hasData: async () => true, + }); + + it('registered data handler', () => { + const dataHandler = getDataHandler('uptime'); + expect(dataHandler?.fetchData).toBeDefined(); + expect(dataHandler?.hasData).toBeDefined(); + }); + + it('returns data when fetchData is called', async () => { + const dataHandler = getDataHandler('uptime'); + const response = await dataHandler?.fetchData(params); + expect(response).toEqual({ + title: 'uptime', + appLink: '/uptime', + stats: { + monitors: { + label: 'Monitors', + type: 'number', + value: 1, + }, + up: { + label: 'Up', + type: 'number', + value: 1, + }, + down: { + label: 'Down', + type: 'number', + value: 1, + }, + }, + series: { + down: { + label: 'Down', + coordinates: [{ x: 1 }], + }, + up: { + label: 'Up', + coordinates: [{ x: 1 }], + }, + }, + }); + }); + + it('returns true when hasData is called', async () => { + const dataHandler = getDataHandler('apm'); + const hasData = await dataHandler?.hasData(); + expect(hasData).toBeTruthy(); + }); + }); + describe('Metrics', () => { + registerDataHandler({ + appName: 'infra_metrics', + fetchData: async () => { + return { + title: 'metrics', + appLink: '/metrics', + stats: { + hosts: { + label: 'hosts', + type: 'number', + value: 1, + }, + cpu: { + label: 'cpu', + type: 'number', + value: 1, + }, + memory: { + label: 'memory', + type: 'number', + value: 1, + }, + disk: { + label: 'disk', + type: 'number', + value: 1, + }, + inboundTraffic: { + label: 'inboundTraffic', + type: 'number', + value: 1, + }, + outboundTraffic: { + label: 'outboundTraffic', + type: 'number', + value: 1, + }, + }, + series: { + inboundTraffic: { + label: 'inbound Traffic', + coordinates: [{ x: 1 }], + }, + outboundTraffic: { + label: 'outbound Traffic', + coordinates: [{ x: 1 }], + }, + }, + }; + }, + hasData: async () => true, + }); + + it('registered data handler', () => { + const dataHandler = getDataHandler('infra_metrics'); + expect(dataHandler?.fetchData).toBeDefined(); + expect(dataHandler?.hasData).toBeDefined(); + }); + + it('returns data when fetchData is called', async () => { + const dataHandler = getDataHandler('infra_metrics'); + const response = await dataHandler?.fetchData(params); + expect(response).toEqual({ + title: 'metrics', + appLink: '/metrics', + stats: { + hosts: { + label: 'hosts', + type: 'number', + value: 1, + }, + cpu: { + label: 'cpu', + type: 'number', + value: 1, + }, + memory: { + label: 'memory', + type: 'number', + value: 1, + }, + disk: { + label: 'disk', + type: 'number', + value: 1, + }, + inboundTraffic: { + label: 'inboundTraffic', + type: 'number', + value: 1, + }, + outboundTraffic: { + label: 'outboundTraffic', + type: 'number', + value: 1, + }, + }, + series: { + inboundTraffic: { + label: 'inbound Traffic', + coordinates: [{ x: 1 }], + }, + outboundTraffic: { + label: 'outbound Traffic', + coordinates: [{ x: 1 }], + }, + }, + }); + }); + + it('returns true when hasData is called', async () => { + const dataHandler = getDataHandler('apm'); + const hasData = await dataHandler?.hasData(); + expect(hasData).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/observability/public/data_handler.ts b/x-pack/plugins/observability/public/data_handler.ts index 8f80f79b2e829..288da3d78bf36 100644 --- a/x-pack/plugins/observability/public/data_handler.ts +++ b/x-pack/plugins/observability/public/data_handler.ts @@ -19,25 +19,27 @@ interface FetchDataParams { export type FetchData = ( fetchDataParams: FetchDataParams ) => Promise; + export type HasData = () => Promise; -interface DataHandler { - fetchData: FetchData; +interface DataHandler { + fetchData: FetchData; hasData: HasData; } const dataHandlers: Partial> = {}; -export type RegisterDataHandler = (params: { - appName: T; - fetchData: FetchData; - hasData: HasData; -}) => void; - -export const registerDataHandler: RegisterDataHandler = ({ appName, fetchData, hasData }) => { +export function registerDataHandler({ + appName, + fetchData, + hasData, +}: { appName: T } & DataHandler) { dataHandlers[appName] = { fetchData, hasData }; -}; +} -export function getDataHandler(appName: ObservabilityApp): DataHandler | undefined { - return dataHandlers[appName]; +export function getDataHandler(appName: T) { + const dataHandler = dataHandlers[appName]; + if (dataHandler) { + return dataHandler as DataHandler; + } } diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index ade347c79728d..fcb569f535d76 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -5,15 +5,15 @@ */ import { PluginInitializerContext, PluginInitializer } from 'kibana/public'; -import { Plugin, ObservabilityPluginSetup, ObservabilityPluginStart } from './plugin'; +import { Plugin, ObservabilityPluginSetup } from './plugin'; -export const plugin: PluginInitializer = ( +export const plugin: PluginInitializer = ( context: PluginInitializerContext ) => { return new Plugin(context); }; -export { ObservabilityPluginSetup, ObservabilityPluginStart }; +export { ObservabilityPluginSetup }; export * from './components/action_menu'; diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 16adf88d152c5..c20e8c7b75d49 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -10,15 +10,13 @@ import { Plugin as PluginClass, PluginInitializerContext, } from '../../../../src/core/public'; -import { RegisterDataHandler, registerDataHandler } from './data_handler'; +import { registerDataHandler } from './data_handler'; export interface ObservabilityPluginSetup { - dashboard: { register: RegisterDataHandler }; + dashboard: { register: typeof registerDataHandler }; } -export type ObservabilityPluginStart = void; - -export class Plugin implements PluginClass { +export class Plugin implements PluginClass { constructor(context: PluginInitializerContext) {} public setup(core: CoreSetup) { From de5ecc0cef35d67efd3a015f3e15bc6bfee90cba Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 26 Jun 2020 11:55:52 +0200 Subject: [PATCH 71/82] [Lens] Add toolbar api (#69263) (#70044) --- .../_workspace_panel_wrapper.scss | 5 +- .../editor_frame/editor_frame.tsx | 30 +++---- .../editor_frame/workspace_panel.tsx | 26 ++++-- .../workspace_panel_wrapper.test.tsx | 65 ++++++++++++++ .../editor_frame/workspace_panel_wrapper.tsx | 89 ++++++++++++++++--- x-pack/plugins/lens/public/types.ts | 11 +++ 6 files changed, 188 insertions(+), 38 deletions(-) create mode 100644 x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.test.tsx diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss index 4ba19cb4ab05b..e663754707e05 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_workspace_panel_wrapper.scss @@ -10,11 +10,14 @@ .lnsWorkspacePanelWrapper__pageContentHeader { @include euiTitle('xs'); padding: $euiSizeM; - border-bottom: $euiBorderThin; // override EuiPage margin-bottom: 0 !important; // sass-lint:disable-line no-important } + .lnsWorkspacePanelWrapper__pageContentHeader--unsaved { + color: $euiTextSubduedColor; + } + .lnsWorkspacePanelWrapper__pageContentBody { @include euiScrollBar; flex-grow: 1; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 07c76a81ed62d..af3d0ed068d2f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -23,7 +23,6 @@ import { WorkspacePanel } from './workspace_panel'; import { Document } from '../../persistence/saved_object_store'; import { RootDragDropProvider } from '../../drag_drop'; import { getSavedObjectFormat } from './save'; -import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { generateId } from '../../id_generator'; import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public'; import { EditorFrameStartPlugins } from '../service'; @@ -275,21 +274,20 @@ export function EditorFrame(props: EditorFrameProps) { } workspacePanel={ allLoaded && ( - - - + ) } suggestionsPanel={ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx index e4d37772eac2e..670afe28293a4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx @@ -37,6 +37,7 @@ import { trackUiEvent } from '../../lens_ui_telemetry'; import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; +import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; export interface WorkspacePanelProps { activeVisualizationId: string | null; @@ -56,6 +57,7 @@ export interface WorkspacePanelProps { ExpressionRenderer: ReactExpressionRendererType; core: CoreStart | CoreSetup; plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart }; + title?: string; } export const WorkspacePanel = debouncedComponent(InnerWorkspacePanel); @@ -73,6 +75,7 @@ export function InnerWorkspacePanel({ core, plugins, ExpressionRenderer: ExpressionRendererComponent, + title, }: WorkspacePanelProps) { const IS_DARK_THEME = core.uiSettings.get('theme:darkMode'); const emptyStateGraphicURL = IS_DARK_THEME @@ -291,13 +294,22 @@ export function InnerWorkspacePanel({ } return ( - - {renderVisualization()} - + + {renderVisualization()} + + ); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.test.tsx new file mode 100644 index 0000000000000..517dff5b5e74c --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Visualization } from '../../types'; +import { createMockVisualization, createMockFramePublicAPI, FrameMock } from '../mocks'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { ReactWrapper } from 'enzyme'; +import { WorkspacePanelWrapper, WorkspacePanelWrapperProps } from './workspace_panel_wrapper'; + +describe('workspace_panel_wrapper', () => { + let mockVisualization: jest.Mocked; + let mockFrameAPI: FrameMock; + let instance: ReactWrapper; + + beforeEach(() => { + mockVisualization = createMockVisualization(); + mockFrameAPI = createMockFramePublicAPI(); + }); + + afterEach(() => { + instance.unmount(); + }); + + it('should render its children', () => { + const MyChild = () => The child elements; + instance = mount( + + + + ); + + expect(instance.find(MyChild)).toHaveLength(1); + }); + + it('should call the toolbar renderer if provided', () => { + const renderToolbarMock = jest.fn(); + const visState = { internalState: 123 }; + instance = mount( + } + activeVisualization={{ ...mockVisualization, renderToolbar: renderToolbarMock }} + emptyExpression={false} + /> + ); + + expect(renderToolbarMock).toHaveBeenCalledWith(expect.any(Element), { + state: visState, + frame: mockFrameAPI, + setState: expect.anything(), + }); + }); +}); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx index cc91510146f35..17461b9fc274f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel_wrapper.tsx @@ -4,25 +4,86 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiPageContent, EuiPageContentHeader, EuiPageContentBody } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import classNames from 'classnames'; +import { + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { FramePublicAPI, Visualization } from '../../types'; +import { NativeRenderer } from '../../native_renderer'; +import { Action } from './state_management'; -interface Props { - title: string; +export interface WorkspacePanelWrapperProps { children: React.ReactNode | React.ReactNode[]; + framePublicAPI: FramePublicAPI; + visualizationState: unknown; + activeVisualization: Visualization | null; + dispatch: (action: Action) => void; + emptyExpression: boolean; + title?: string; } -export function WorkspacePanelWrapper({ children, title }: Props) { +export function WorkspacePanelWrapper({ + children, + framePublicAPI, + visualizationState, + activeVisualization, + dispatch, + title, + emptyExpression, +}: WorkspacePanelWrapperProps) { + const setVisualizationState = useCallback( + (newState: unknown) => { + if (!activeVisualization) { + return; + } + dispatch({ + type: 'UPDATE_VISUALIZATION_STATE', + visualizationId: activeVisualization.id, + newState, + clearStagedPreview: false, + }); + }, + [dispatch] + ); return ( - - {title && ( - - {title} - + + {activeVisualization && activeVisualization.renderToolbar && ( + + + )} - - {children} - - + + + {(!emptyExpression || title) && ( + + + {title || + i18n.translate('xpack.lens.chartTitle.unsaved', { defaultMessage: 'Unsaved' })} + + + )} + + {children} + + + + ); } diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index c2437aa3cc3cc..d451e312446bd 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -290,6 +290,12 @@ export type VisualizationLayerWidgetProps = VisualizationConfigProp setState: (newState: T) => void; }; +export interface VisualizationToolbarProps { + setState: (newState: T) => void; + frame: FramePublicAPI; + state: T; +} + export type VisualizationDimensionEditorProps = VisualizationConfigProps & { groupId: string; accessor: string; @@ -454,6 +460,11 @@ export interface Visualization { * for extra configurability, such as for styling the legend or axis */ renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; + /** + * Toolbar rendered above the visualization. This is meant to be used to provide chart-level + * settings for the visualization. + */ + renderToolbar?: (domElement: Element, props: VisualizationToolbarProps) => void; /** * Visualizations can provide a custom icon which will open a layer-specific popover * If no icon is provided, gear icon is default From 4b0e45d7c872c2053f56a8fa3b54af810a33a453 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Fri, 26 Jun 2020 11:59:00 +0200 Subject: [PATCH 72/82] [Lens] Fix delete button position in dimension panel for long labels (#69495) (#70046) --- .../editor_frame/config_panel/_dimension_popover.scss | 2 ++ .../lens/public/indexpattern_datasource/_field_item.scss | 1 + 2 files changed, 3 insertions(+) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss index 254807d06d386..691cda9ff0d79 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss @@ -1,9 +1,11 @@ .lnsDimensionPopover { line-height: 0; flex-grow: 1; + max-width: calc(100% - #{$euiSizeL}); } .lnsDimensionPopover__trigger { max-width: 100%; display: block; + word-break: break-word; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss b/x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss index 41919b900c71f..6e51c45ad02c1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/_field_item.scss @@ -29,6 +29,7 @@ .lnsFieldItem__name { margin-left: $euiSizeS; flex-grow: 1; + word-break: break-word; } .lnsFieldListPanel__fieldIcon, From 37690e5a275b88b13423057d28c519d8aa0ab180 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Fri, 26 Jun 2020 13:05:17 +0300 Subject: [PATCH 73/82] Fixes bug on color picker defaults on TSVB (#69889) (#70042) * Fixes bug on color picker defaults on TSVB * Add test to ensure that the input text of the picker is set up correctly --- .../components/color_picker.test.tsx | 18 ++++++++++++++++++ .../application/components/color_picker.tsx | 6 ++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx index ca8750a991d83..7c930fa2e2960 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx @@ -22,6 +22,8 @@ import { ColorPicker, ColorPickerProps } from './color_picker'; import { mount } from 'enzyme'; import { ReactWrapper } from 'enzyme'; import { EuiColorPicker, EuiIconTip } from '@elastic/eui'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; describe('ColorPicker', () => { const defaultProps: ColorPickerProps = { @@ -42,6 +44,22 @@ describe('ColorPicker', () => { expect(component.find('.tvbColorPicker__clear').length).toBe(0); }); + it('should render the correct value to the input text if the prop value is hex', () => { + const props = { ...defaultProps, value: '#68BC00' }; + component = mount(); + component.find('.tvbColorPicker button').simulate('click'); + const input = findTestSubject(component, 'topColorPickerInput'); + expect(input.props().value).toBe('#68BC00'); + }); + + it('should render the correct value to the input text if the prop value is rgba', () => { + const props = { ...defaultProps, value: 'rgba(85,66,177,1)' }; + component = mount(); + component.find('.tvbColorPicker button').simulate('click'); + const input = findTestSubject(component, 'topColorPickerInput'); + expect(input.props().value).toBe('85,66,177,1'); + }); + it('should render the correct aria label to the color swatch button', () => { const props = { ...defaultProps, value: 'rgba(85,66,177,0.59)' }; component = mount(); diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx index be580c80d5941..444e5c90c7a6d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/color_picker.tsx @@ -43,8 +43,10 @@ export interface ColorPickerProps { } export function ColorPicker({ name, value, disableTrash = false, onChange }: ColorPickerProps) { - const initialColorValue = value ? value.replace(COMMAS_NUMS_ONLY_RE, '') : ''; - const [color, setColor] = useState(initialColorValue); + const initialColorValue = value?.includes('rgba') + ? value.replace(COMMAS_NUMS_ONLY_RE, '') + : value; + const [color, setColor] = useState(initialColorValue || ''); const handleColorChange: EuiColorPickerProps['onChange'] = (text: string, { rgba, hex }) => { setColor(text); From e365aadfc9948a161baa90b1bdf92aceedfff443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Fri, 26 Jun 2020 11:51:09 +0100 Subject: [PATCH 74/82] [APM]Create API to return data to be used on the Overview page (#69137) (#70049) * Adding apm data fetcher * removing error rate * chaging observability dashboard routes * APM observability fetch data * fixing imports * adding unit test * addressing PR comments * adding processor event in the query, and refactoring theme * fixing ts issues * fixing unit tests --- x-pack/plugins/apm/public/plugin.ts | 18 +++ .../rest/observability.dashboard.test.ts | 121 ++++++++++++++++++ .../services/rest/observability_dashboard.ts | 71 ++++++++++ x-pack/plugins/apm/public/utils/get_theme.ts | 13 ++ .../get_service_count.ts | 52 ++++++++ .../get_transaction_coordinates.ts | 57 +++++++++ .../lib/observability_dashboard/has_data.ts | 26 ++++ .../apm/server/routes/create_apm_api.ts | 10 +- .../server/routes/observability_dashboard.ts | 41 ++++++ .../observability/public/data_handler.ts | 2 +- 10 files changed, 409 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts create mode 100644 x-pack/plugins/apm/public/services/rest/observability_dashboard.ts create mode 100644 x-pack/plugins/apm/public/utils/get_theme.ts create mode 100644 x-pack/plugins/apm/server/lib/observability_dashboard/get_service_count.ts create mode 100644 x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts create mode 100644 x-pack/plugins/apm/server/lib/observability_dashboard/has_data.ts create mode 100644 x-pack/plugins/apm/server/routes/observability_dashboard.ts diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index e9de8fcd890d0..0e495391c94f2 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -38,6 +38,11 @@ import { setHelpExtension } from './setHelpExtension'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; import { setReadonlyBadge } from './updateBadge'; import { createStaticIndexPattern } from './services/rest/index_pattern'; +import { + fetchLandingPageData, + hasData, +} from './services/rest/observability_dashboard'; +import { getTheme } from './utils/get_theme'; export type ApmPluginSetup = void; export type ApmPluginStart = void; @@ -73,6 +78,19 @@ export class ApmPlugin implements Plugin { pluginSetupDeps.home.environment.update({ apmUi: true }); pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry); + if (plugins.observability) { + const theme = getTheme({ + isDarkMode: core.uiSettings.get('theme:darkMode'), + }); + plugins.observability.dashboard.register({ + appName: 'apm', + fetchData: async (params) => { + return fetchLandingPageData(params, { theme }); + }, + hasData, + }); + } + core.application.register({ id: 'apm', title: 'APM', diff --git a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts new file mode 100644 index 0000000000000..1ee8d79ee99a5 --- /dev/null +++ b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fetchLandingPageData, hasData } from './observability_dashboard'; +import * as createCallApmApi from './createCallApmApi'; +import { getTheme } from '../../utils/get_theme'; + +const theme = getTheme({ isDarkMode: false }); + +describe('Observability dashboard data', () => { + const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi'); + afterEach(() => { + callApmApiMock.mockClear(); + }); + describe('hasData', () => { + it('returns false when no data is available', async () => { + callApmApiMock.mockImplementation(() => Promise.resolve(false)); + const response = await hasData(); + expect(response).toBeFalsy(); + }); + it('returns true when data is available', async () => { + callApmApiMock.mockImplementation(() => Promise.resolve(true)); + const response = await hasData(); + expect(response).toBeTruthy(); + }); + }); + + describe('fetchLandingPageData', () => { + it('returns APM data with series and stats', async () => { + callApmApiMock.mockImplementation(() => + Promise.resolve({ + serviceCount: 10, + transactionCoordinates: [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 3 }, + ], + }) + ); + const response = await fetchLandingPageData( + { + startTime: '1', + endTime: '2', + bucketSize: '3', + }, + { theme } + ); + expect(response).toEqual({ + title: 'APM', + appLink: '/app/apm', + stats: { + services: { + type: 'number', + label: 'Services', + value: 10, + }, + transactions: { + type: 'number', + label: 'Transactions', + value: 6, + color: '#6092c0', + }, + }, + series: { + transactions: { + label: 'Transactions', + coordinates: [ + { x: 1, y: 1 }, + { x: 2, y: 2 }, + { x: 3, y: 3 }, + ], + color: '#6092c0', + }, + }, + }); + }); + it('returns empty transaction coordinates', async () => { + callApmApiMock.mockImplementation(() => + Promise.resolve({ + serviceCount: 0, + transactionCoordinates: [], + }) + ); + const response = await fetchLandingPageData( + { + startTime: '1', + endTime: '2', + bucketSize: '3', + }, + { theme } + ); + expect(response).toEqual({ + title: 'APM', + appLink: '/app/apm', + stats: { + services: { + type: 'number', + label: 'Services', + value: 0, + }, + transactions: { + type: 'number', + label: 'Transactions', + value: 0, + color: '#6092c0', + }, + }, + series: { + transactions: { + label: 'Transactions', + coordinates: [], + color: '#6092c0', + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts new file mode 100644 index 0000000000000..2221904932b63 --- /dev/null +++ b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { sum } from 'lodash'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FetchDataParams } from '../../../../observability/public/data_handler'; +import { ApmFetchDataResponse } from '../../../../observability/public/typings/fetch_data_response'; +import { callApmApi } from './createCallApmApi'; +import { Theme } from '../../utils/get_theme'; + +interface Options { + theme: Theme; +} + +export const fetchLandingPageData = async ( + { startTime, endTime, bucketSize }: FetchDataParams, + { theme }: Options +): Promise => { + const data = await callApmApi({ + pathname: '/api/apm/observability_dashboard', + params: { query: { start: startTime, end: endTime, bucketSize } }, + }); + + const { serviceCount, transactionCoordinates } = data; + + return { + title: i18n.translate('xpack.apm.observabilityDashboard.title', { + defaultMessage: 'APM', + }), + appLink: '/app/apm', + stats: { + services: { + type: 'number', + label: i18n.translate( + 'xpack.apm.observabilityDashboard.stats.services', + { defaultMessage: 'Services' } + ), + value: serviceCount, + }, + transactions: { + type: 'number', + label: i18n.translate( + 'xpack.apm.observabilityDashboard.stats.transactions', + { defaultMessage: 'Transactions' } + ), + value: sum(transactionCoordinates.map((coordinates) => coordinates.y)), + color: theme.euiColorVis1, + }, + }, + series: { + transactions: { + label: i18n.translate( + 'xpack.apm.observabilityDashboard.chart.transactions', + { defaultMessage: 'Transactions' } + ), + color: theme.euiColorVis1, + coordinates: transactionCoordinates, + }, + }, + }; +}; + +export async function hasData() { + return await callApmApi({ + pathname: '/api/apm/observability_dashboard/has_data', + }); +} diff --git a/x-pack/plugins/apm/public/utils/get_theme.ts b/x-pack/plugins/apm/public/utils/get_theme.ts new file mode 100644 index 0000000000000..e5020202b7721 --- /dev/null +++ b/x-pack/plugins/apm/public/utils/get_theme.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; + +export type Theme = ReturnType; + +export function getTheme({ isDarkMode }: { isDarkMode: boolean }) { + return isDarkMode ? darkTheme : lightTheme; +} diff --git a/x-pack/plugins/apm/server/lib/observability_dashboard/get_service_count.ts b/x-pack/plugins/apm/server/lib/observability_dashboard/get_service_count.ts new file mode 100644 index 0000000000000..4c4d058c7139d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/observability_dashboard/get_service_count.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ProcessorEvent } from '../../../common/processor_event'; +import { rangeFilter } from '../../../common/utils/range_filter'; +import { + SERVICE_NAME, + PROCESSOR_EVENT, +} from '../../../common/elasticsearch_fieldnames'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; + +export async function getServiceCount({ + setup, +}: { + setup: Setup & SetupTimeRange; +}) { + const { client, indices, start, end } = setup; + + const params = { + index: [ + indices['apm_oss.transactionIndices'], + indices['apm_oss.errorIndices'], + indices['apm_oss.metricsIndices'], + ], + body: { + size: 0, + query: { + bool: { + filter: [ + { range: rangeFilter(start, end) }, + { + terms: { + [PROCESSOR_EVENT]: [ + ProcessorEvent.error, + ProcessorEvent.transaction, + ProcessorEvent.metric, + ], + }, + }, + ], + }, + }, + aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } }, + }, + }; + + const { aggregations } = await client.search(params); + return aggregations?.serviceCount.value || 0; +} diff --git a/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts new file mode 100644 index 0000000000000..78ed11d839ad2 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { rangeFilter } from '../../../common/utils/range_filter'; +import { Coordinates } from '../../../../observability/public/typings/fetch_data_response'; +import { PROCESSOR_EVENT } from '../../../common/elasticsearch_fieldnames'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { ProcessorEvent } from '../../../common/processor_event'; + +export async function getTransactionCoordinates({ + setup, + bucketSize, +}: { + setup: Setup & SetupTimeRange; + bucketSize: string; +}): Promise { + const { client, indices, start, end } = setup; + + const { aggregations } = await client.search({ + index: indices['apm_oss.transactionIndices'], + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + { range: rangeFilter(start, end) }, + ], + }, + }, + aggs: { + distribution: { + date_histogram: { + field: '@timestamp', + fixed_interval: bucketSize, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }, + }, + }, + }, + }); + + return ( + aggregations?.distribution.buckets.map((bucket) => ({ + x: bucket.key, + y: bucket.doc_count, + })) || [] + ); +} diff --git a/x-pack/plugins/apm/server/lib/observability_dashboard/has_data.ts b/x-pack/plugins/apm/server/lib/observability_dashboard/has_data.ts new file mode 100644 index 0000000000000..73cc2d273ec69 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/observability_dashboard/has_data.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Setup } from '../helpers/setup_request'; + +export async function hasData({ setup }: { setup: Setup }) { + const { client, indices } = setup; + try { + const params = { + index: [ + indices['apm_oss.transactionIndices'], + indices['apm_oss.errorIndices'], + indices['apm_oss.metricsIndices'], + ], + terminateAfter: 1, + size: 0, + }; + + const response = await client.search(params); + return response.hits.total.value > 0; + } catch (e) { + return false; + } +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index a34690aff43b4..02be2e7e4dcdf 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -76,6 +76,10 @@ import { rumPageViewsTrendRoute, rumPageLoadDistributionRoute, } from './rum_client'; +import { + observabilityDashboardHasDataRoute, + observabilityDashboardDataRoute, +} from './observability_dashboard'; const createApmApi = () => { const api = createApi() @@ -160,7 +164,11 @@ const createApmApi = () => { .add(rumOverviewLocalFiltersRoute) .add(rumPageViewsTrendRoute) .add(rumPageLoadDistributionRoute) - .add(rumClientMetricsRoute); + .add(rumClientMetricsRoute) + + // Observability dashboard + .add(observabilityDashboardHasDataRoute) + .add(observabilityDashboardDataRoute); return api; }; diff --git a/x-pack/plugins/apm/server/routes/observability_dashboard.ts b/x-pack/plugins/apm/server/routes/observability_dashboard.ts new file mode 100644 index 0000000000000..10c74295fe3e4 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/observability_dashboard.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { hasData } from '../lib/observability_dashboard/has_data'; +import { createRoute } from './create_route'; +import { rangeRt } from './default_api_types'; +import { getServiceCount } from '../lib/observability_dashboard/get_service_count'; +import { getTransactionCoordinates } from '../lib/observability_dashboard/get_transaction_coordinates'; + +export const observabilityDashboardHasDataRoute = createRoute(() => ({ + path: '/api/apm/observability_dashboard/has_data', + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + return await hasData({ setup }); + }, +})); + +export const observabilityDashboardDataRoute = createRoute(() => ({ + path: '/api/apm/observability_dashboard', + params: { + query: t.intersection([rangeRt, t.type({ bucketSize: t.string })]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { bucketSize } = context.params.query; + const serviceCountPromise = getServiceCount({ setup }); + const transactionCoordinatesPromise = getTransactionCoordinates({ + setup, + bucketSize, + }); + const [serviceCount, transactionCoordinates] = await Promise.all([ + serviceCountPromise, + transactionCoordinatesPromise, + ]); + return { serviceCount, transactionCoordinates }; + }, +})); diff --git a/x-pack/plugins/observability/public/data_handler.ts b/x-pack/plugins/observability/public/data_handler.ts index 288da3d78bf36..65f2c52a4e320 100644 --- a/x-pack/plugins/observability/public/data_handler.ts +++ b/x-pack/plugins/observability/public/data_handler.ts @@ -7,7 +7,7 @@ import { ObservabilityFetchDataResponse, FetchDataResponse } from './typings/fetch_data_response'; import { ObservabilityApp } from '../typings/common'; -interface FetchDataParams { +export interface FetchDataParams { // The start timestamp in milliseconds of the queried time interval startTime: string; // The end timestamp in milliseconds of the queried time interval From 7b6c41b45faf3fe47951dec220f5a4ce2bf2f558 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 26 Jun 2020 08:35:04 -0400 Subject: [PATCH 75/82] [Ingest Manager] Fix typo in constant name (#69919) (#70032) --- x-pack/plugins/ingest_manager/server/constants/index.ts | 2 +- x-pack/plugins/ingest_manager/server/saved_objects/index.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index 4d60b9031414e..ebcce6320ec4b 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -36,7 +36,7 @@ export { PACKAGES_SAVED_OBJECT_TYPE, INDEX_PATTERN_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - GLOBAL_SETTINGS_SAVED_OBJECT_TYPE as GLOBAL_SETTINGS_SAVED_OBJET_TYPE, + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, // Defaults DEFAULT_AGENT_CONFIG, DEFAULT_OUTPUT, diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index 703ddb521c831..9afc06c193a3b 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -15,7 +15,7 @@ import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - GLOBAL_SETTINGS_SAVED_OBJET_TYPE, + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, } from '../constants'; import { migrateDatasourcesToV790 } from './migrations/datasources_v790'; import { migrateAgentConfigToV790 } from './migrations/agent_config_v790'; @@ -26,8 +26,8 @@ import { migrateAgentConfigToV790 } from './migrations/agent_config_v790'; */ const savedObjectTypes: { [key: string]: SavedObjectsType } = { - [GLOBAL_SETTINGS_SAVED_OBJET_TYPE]: { - name: GLOBAL_SETTINGS_SAVED_OBJET_TYPE, + [GLOBAL_SETTINGS_SAVED_OBJECT_TYPE]: { + name: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, hidden: false, namespaceType: 'agnostic', management: { From fed64c8ea2308a8278eb79df86f4821626f6d067 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 26 Jun 2020 06:45:09 -0600 Subject: [PATCH 76/82] [Maps] Fix icon palettes are not working (#69937) (#70035) * [Maps] Fix icon palettes are not working * unit test mapbox icon-image expression * fix unit test expect statements --- x-pack/plugins/maps/common/constants.ts | 12 ++++ .../ems_file_source/ems_file_source.tsx | 7 +- .../es_geo_grid_source/es_geo_grid_source.js | 6 +- .../es_pew_pew_source/es_pew_pew_source.js | 5 +- .../es_search_source/es_search_source.js | 6 +- .../mvt_single_layer_vector_source.ts | 7 +- .../classes/sources/vector_feature_types.ts | 11 --- .../sources/vector_source/vector_source.d.ts | 4 +- .../sources/vector_source/vector_source.js | 4 +- .../vector/components/vector_style_editor.js | 24 +++---- .../dynamic_icon_property.test.tsx.snap | 20 +++++- .../properties/dynamic_color_property.js | 4 +- .../properties/dynamic_icon_property.js | 2 +- .../properties/dynamic_icon_property.test.tsx | 58 ++++++++++++--- .../properties/dynamic_size_property.js | 8 ++- ...{style_util.test.js => style_util.test.ts} | 72 ++++++++++++------- .../vector/{style_util.js => style_util.ts} | 45 ++++++++---- .../classes/styles/vector/symbol_utils.js | 2 +- .../classes/styles/vector/vector_style.js | 26 +++---- .../styles/vector/vector_style.test.js | 5 +- 20 files changed, 211 insertions(+), 117 deletions(-) delete mode 100644 x-pack/plugins/maps/public/classes/sources/vector_feature_types.ts rename x-pack/plugins/maps/public/classes/styles/vector/{style_util.test.js => style_util.test.ts} (60%) rename x-pack/plugins/maps/public/classes/styles/vector/{style_util.js => style_util.ts} (57%) diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index ea722c18e7005..bf30006441c9d 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -232,3 +232,15 @@ export enum LAYER_WIZARD_CATEGORY { REFERENCE = 'REFERENCE', SOLUTIONS = 'SOLUTIONS', } + +export enum VECTOR_SHAPE_TYPE { + POINT = 'POINT', + LINE = 'LINE', + POLYGON = 'POLYGON', +} + +// https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#data-expressions +export enum MB_LOOKUP_FUNCTION { + GET = 'get', + FEATURE_STATE = 'feature-state', +} diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx index f7fb0078764c4..f55a7434d1217 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx @@ -11,8 +11,7 @@ import { Adapters } from 'src/plugins/inspector/public'; import { FileLayer } from '@elastic/ems-client'; import { Attribution, ImmutableSourceProperty, SourceEditorArgs } from '../source'; import { AbstractVectorSource, GeoJsonWithMeta, IVectorSource } from '../vector_source'; -import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; -import { SOURCE_TYPES, FIELD_ORIGIN } from '../../../../common/constants'; +import { SOURCE_TYPES, FIELD_ORIGIN, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; import { getEmsFileLayers } from '../../../meta'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { UpdateSourceEditor } from './update_source_editor'; @@ -179,8 +178,8 @@ export class EMSFileSource extends AbstractVectorSource implements IEmsFileSourc return Promise.all(promises); } - async getSupportedShapeTypes(): Promise { - return [VECTOR_SHAPE_TYPES.POLYGON]; + async getSupportedShapeTypes(): Promise { + return [VECTOR_SHAPE_TYPE.POLYGON]; } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js index c05c1f2dd7c1e..b613f577067ba 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js @@ -7,7 +7,6 @@ import React from 'react'; import uuid from 'uuid/v4'; -import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson } from './convert_to_geojson'; import { UpdateSourceEditor } from './update_source_editor'; import { @@ -15,6 +14,7 @@ import { DEFAULT_MAX_BUCKETS_LIMIT, RENDER_AS, GRID_RESOLUTION, + VECTOR_SHAPE_TYPE, } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -326,10 +326,10 @@ export class ESGeoGridSource extends AbstractESAggSource { async getSupportedShapeTypes() { if (this._descriptor.requestType === RENDER_AS.GRID) { - return [VECTOR_SHAPE_TYPES.POLYGON]; + return [VECTOR_SHAPE_TYPE.POLYGON]; } - return [VECTOR_SHAPE_TYPES.POINT]; + return [VECTOR_SHAPE_TYPE.POINT]; } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index fda73bc0f73a0..076e7a758a4fb 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -7,10 +7,9 @@ import React from 'react'; import uuid from 'uuid/v4'; -import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; -import { SOURCE_TYPES } from '../../../../common/constants'; +import { SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { convertToLines } from './convert_to_lines'; import { AbstractESAggSource } from '../es_agg_source'; @@ -61,7 +60,7 @@ export class ESPewPewSource extends AbstractESAggSource { } async getSupportedShapeTypes() { - return [VECTOR_SHAPE_TYPES.LINE]; + return [VECTOR_SHAPE_TYPE.LINE]; } async getImmutableProperties() { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js index 51dd57ffad0d1..c8f14f1dc6a4b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js @@ -7,7 +7,6 @@ import _ from 'lodash'; import React from 'react'; -import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { AbstractESSource } from '../es_source'; import { getSearchService } from '../../../kibana_services'; import { hitsToGeoJson } from '../../../elasticsearch_geo_utils'; @@ -18,6 +17,7 @@ import { DEFAULT_MAX_BUCKETS_LIMIT, SORT_ORDER, SCALING_TYPES, + VECTOR_SHAPE_TYPE, } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -471,10 +471,10 @@ export class ESSearchSource extends AbstractESSource { } if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { - return [VECTOR_SHAPE_TYPES.POINT]; + return [VECTOR_SHAPE_TYPE.POINT]; } - return [VECTOR_SHAPE_TYPES.POINT, VECTOR_SHAPE_TYPES.LINE, VECTOR_SHAPE_TYPES.POLYGON]; + return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; } getSourceTooltipContent(sourceDataRequest) { diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts index 86a1589a7a030..03b91df22d3ca 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts @@ -8,8 +8,7 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { AbstractSource, ImmutableSourceProperty } from '../source'; import { BoundsFilters, GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; -import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES } from '../../../../common/constants'; -import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; +import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; import { IField } from '../../fields/field'; import { registerSource } from '../source_registry'; import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; @@ -116,8 +115,8 @@ export class MVTSingleLayerVectorSource extends AbstractSource }; } - async getSupportedShapeTypes(): Promise { - return [VECTOR_SHAPE_TYPES.POINT, VECTOR_SHAPE_TYPES.LINE, VECTOR_SHAPE_TYPES.POLYGON]; + async getSupportedShapeTypes(): Promise { + return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; } canFormatFeatureProperties() { diff --git a/x-pack/plugins/maps/public/classes/sources/vector_feature_types.ts b/x-pack/plugins/maps/public/classes/sources/vector_feature_types.ts deleted file mode 100644 index 9f03357e17dad..0000000000000 --- a/x-pack/plugins/maps/public/classes/sources/vector_feature_types.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export enum VECTOR_SHAPE_TYPES { - POINT = 'POINT', - LINE = 'LINE', - POLYGON = 'POLYGON', -} diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts index 711b7d600d74d..99a7478cd8362 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts @@ -16,7 +16,7 @@ import { VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; -import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; +import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; import { ITooltipProperty } from '../../tooltips/tooltip_property'; export type GeoJsonFetchMeta = ESSearchSourceResponseMeta; @@ -68,7 +68,7 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc getFields(): Promise; getFieldByName(fieldName: string): IField | null; getSyncMeta(): VectorSourceSyncMeta; - getSupportedShapeTypes(): Promise; + getSupportedShapeTypes(): Promise; canFormatFeatureProperties(): boolean; getApplyGlobalQuery(): boolean; getFieldNames(): string[]; diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js index ccf6c7963c9b4..ecb13bb875721 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js @@ -9,7 +9,7 @@ import { AbstractSource } from './../source'; import * as topojson from 'topojson-client'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { VECTOR_SHAPE_TYPES } from './../vector_feature_types'; +import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; export class AbstractVectorSource extends AbstractSource { static async getGeoJson({ format, featureCollectionPath, fetchUrl }) { @@ -127,7 +127,7 @@ export class AbstractVectorSource extends AbstractSource { } async getSupportedShapeTypes() { - return [VECTOR_SHAPE_TYPES.POINT, VECTOR_SHAPE_TYPES.LINE, VECTOR_SHAPE_TYPES.POLYGON]; + return [VECTOR_SHAPE_TYPE.POINT, VECTOR_SHAPE_TYPE.LINE, VECTOR_SHAPE_TYPE.POLYGON]; } getSourceTooltipContent(/* sourceDataRequest */) { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js index 3424a972fed06..7856a4ddaff39 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js @@ -16,7 +16,6 @@ import { VectorStyleLabelBorderSizeEditor } from './label/vector_style_label_bor import { OrientationEditor } from './orientation/orientation_editor'; import { getDefaultDynamicProperties, getDefaultStaticProperties } from '../vector_style_defaults'; import { DEFAULT_FILL_COLORS, DEFAULT_LINE_COLORS } from '../../color_utils'; -import { VECTOR_SHAPE_TYPES } from '../../../sources/vector_feature_types'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiButtonGroup, EuiFormRow, EuiSwitch } from '@elastic/eui'; @@ -26,6 +25,7 @@ import { LABEL_BORDER_SIZES, VECTOR_STYLES, STYLE_TYPE, + VECTOR_SHAPE_TYPE, } from '../../../../../common/constants'; export class VectorStyleEditor extends Component { @@ -96,11 +96,11 @@ export class VectorStyleEditor extends Component { } if (this.state.selectedFeature === null) { - let selectedFeature = VECTOR_SHAPE_TYPES.POLYGON; + let selectedFeature = VECTOR_SHAPE_TYPE.POLYGON; if (this.props.isPointsOnly) { - selectedFeature = VECTOR_SHAPE_TYPES.POINT; + selectedFeature = VECTOR_SHAPE_TYPE.POINT; } else if (this.props.isLinesOnly) { - selectedFeature = VECTOR_SHAPE_TYPES.LINE; + selectedFeature = VECTOR_SHAPE_TYPE.LINE; } this.setState({ selectedFeature: selectedFeature, @@ -414,30 +414,30 @@ export class VectorStyleEditor extends Component { if (supportedFeatures.length === 1) { switch (supportedFeatures[0]) { - case VECTOR_SHAPE_TYPES.POINT: + case VECTOR_SHAPE_TYPE.POINT: return this._renderPointProperties(); - case VECTOR_SHAPE_TYPES.LINE: + case VECTOR_SHAPE_TYPE.LINE: return this._renderLineProperties(); - case VECTOR_SHAPE_TYPES.POLYGON: + case VECTOR_SHAPE_TYPE.POLYGON: return this._renderPolygonProperties(); } } const featureButtons = [ { - id: VECTOR_SHAPE_TYPES.POINT, + id: VECTOR_SHAPE_TYPE.POINT, label: i18n.translate('xpack.maps.vectorStyleEditor.pointLabel', { defaultMessage: 'Points', }), }, { - id: VECTOR_SHAPE_TYPES.LINE, + id: VECTOR_SHAPE_TYPE.LINE, label: i18n.translate('xpack.maps.vectorStyleEditor.lineLabel', { defaultMessage: 'Lines', }), }, { - id: VECTOR_SHAPE_TYPES.POLYGON, + id: VECTOR_SHAPE_TYPE.POLYGON, label: i18n.translate('xpack.maps.vectorStyleEditor.polygonLabel', { defaultMessage: 'Polygons', }), @@ -445,9 +445,9 @@ export class VectorStyleEditor extends Component { ]; let styleProperties = this._renderPolygonProperties(); - if (selectedFeature === VECTOR_SHAPE_TYPES.LINE) { + if (selectedFeature === VECTOR_SHAPE_TYPE.LINE) { styleProperties = this._renderLineProperties(); - } else if (selectedFeature === VECTOR_SHAPE_TYPES.POINT) { + } else if (selectedFeature === VECTOR_SHAPE_TYPE.POINT) { styleProperties = this._renderPointProperties(); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_icon_property.test.tsx.snap b/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_icon_property.test.tsx.snap index b4843324a0def..631a6117a111d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_icon_property.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_icon_property.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Should render categorical legend with breaks 1`] = ` +exports[`renderLegendDetailRow Should render categorical legend with breaks 1`] = `
+ + + Other + + } + styleName="icon" + symbolId="square" + /> +
`; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js index 4c02dee762e9d..556bb2b79e836 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js @@ -13,7 +13,7 @@ import { GRADIENT_INTERVALS, } from '../../color_utils'; import React from 'react'; -import { COLOR_MAP_TYPE } from '../../../../../common/constants'; +import { COLOR_MAP_TYPE, MB_LOOKUP_FUNCTION } from '../../../../../common/constants'; import { isCategoricalStopsInvalid, getOtherCategoryLabel, @@ -152,7 +152,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { makeMbClampedNumberExpression({ minValue: rangeFieldMeta.min, maxValue: rangeFieldMeta.max, - lookupFunction: 'feature-state', + lookupFunction: MB_LOOKUP_FUNCTION.FEATURE_STATE, fallback: lessThanFirstStopValue, fieldName: targetName, }), diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js index c7620512710dc..665317569e5e8 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js @@ -23,7 +23,7 @@ export class DynamicIconProperty extends DynamicStyleProperty { getNumberOfCategories() { const palette = getIconPalette(this._options.iconPaletteId); - return palette ? palette.length : 0; + return palette.length; } syncIconWithMb(symbolLayerId, mbMap, iconPixelSize) { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx index 505c08ac35ba7..132c0b3f27603 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx @@ -34,8 +34,8 @@ const makeProperty = (options: Partial, field: IField = mock ); }; -describe('DynamicIconProperty', () => { - it('should derive category number from palettes', async () => { +describe('getNumberOfCategories', () => { + test('should derive category number from palettes', async () => { const filled = makeProperty({ iconPaletteId: 'filledShapes', }); @@ -47,15 +47,53 @@ describe('DynamicIconProperty', () => { }); }); -test('Should render categorical legend with breaks', async () => { - const iconStyle = makeProperty({ - iconPaletteId: 'filledShapes', +describe('renderLegendDetailRow', () => { + test('Should render categorical legend with breaks', async () => { + const iconStyle = makeProperty({ + iconPaletteId: 'filledShapes', + }); + + const legendRow = iconStyle.renderLegendDetailRow({ isPointsOnly: true, isLinesOnly: false }); + const component = shallow(legendRow); + await new Promise((resolve) => process.nextTick(resolve)); + component.update(); + + expect(component).toMatchSnapshot(); }); +}); - const legendRow = iconStyle.renderLegendDetailRow({ isPointsOnly: true, isLinesOnly: false }); - const component = shallow(legendRow); - await new Promise((resolve) => process.nextTick(resolve)); - component.update(); +describe('get mapbox icon-image expression (via internal _getMbIconImageExpression)', () => { + describe('categorical icon palette', () => { + test('should return mapbox expression for pre-defined icon palette', async () => { + const iconStyle = makeProperty({ + iconPaletteId: 'filledShapes', + }); + expect(iconStyle._getMbIconImageExpression(15)).toEqual([ + 'match', + ['to-string', ['get', 'foobar']], + 'US', + 'circle-15', + 'CN', + 'marker-15', + 'square-15', + ]); + }); - expect(component).toMatchSnapshot(); + test('should return mapbox expression for custom icon palette', async () => { + const iconStyle = makeProperty({ + useCustomIconMap: true, + customIconStops: [ + { stop: null, icon: 'circle' }, + { stop: 'MX', icon: 'marker' }, + ], + }); + expect(iconStyle._getMbIconImageExpression(15)).toEqual([ + 'match', + ['to-string', ['get', 'foobar']], + 'MX', + 'marker-15', + 'circle-15', + ]); + }); + }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js index a0af2fbb939d8..662d1ccf33b95 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js @@ -12,7 +12,7 @@ import { LARGE_MAKI_ICON_SIZE, SMALL_MAKI_ICON_SIZE, } from '../symbol_utils'; -import { VECTOR_STYLES } from '../../../../../common/constants'; +import { MB_LOOKUP_FUNCTION, VECTOR_STYLES } from '../../../../../common/constants'; import _ from 'lodash'; import React from 'react'; @@ -60,7 +60,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty { minValue: rangeFieldMeta.min, maxValue: rangeFieldMeta.max, fallback: 0, - lookupFunction: 'get', + lookupFunction: MB_LOOKUP_FUNCTION.GET, fieldName: targetName, }), rangeFieldMeta.min, @@ -109,7 +109,9 @@ export class DynamicSizeProperty extends DynamicStyleProperty { } _getMbDataDrivenSize({ targetName, minSize, maxSize, minValue, maxValue }) { - const lookup = this.supportsMbFeatureState() ? 'feature-state' : 'get'; + const lookup = this.supportsMbFeatureState() + ? MB_LOOKUP_FUNCTION.FEATURE_STATE + : MB_LOOKUP_FUNCTION.GET; const stops = minValue === maxValue ? [maxValue, maxSize] : [minValue, minSize, maxValue, maxSize]; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.js b/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.ts similarity index 60% rename from x-pack/plugins/maps/public/classes/styles/vector/style_util.test.js rename to x-pack/plugins/maps/public/classes/styles/vector/style_util.test.ts index eb4c6708fb2dd..6c1f060383d05 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.ts @@ -5,58 +5,67 @@ */ import { isOnlySingleFeatureType, assignCategoriesToPalette, dynamicRound } from './style_util'; -import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; +import { VECTOR_SHAPE_TYPE } from '../../../../common/constants'; describe('isOnlySingleFeatureType', () => { describe('source supports single feature type', () => { - const supportedFeatures = [VECTOR_SHAPE_TYPES.POINT]; + const supportedFeatures = [VECTOR_SHAPE_TYPE.POINT]; + const hasFeatureType = { + [VECTOR_SHAPE_TYPE.POINT]: false, + [VECTOR_SHAPE_TYPE.LINE]: false, + [VECTOR_SHAPE_TYPE.POLYGON]: false, + }; test('Is only single feature type when only supported feature type is target feature type', () => { - expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT, supportedFeatures)).toBe(true); + expect( + isOnlySingleFeatureType(VECTOR_SHAPE_TYPE.POINT, supportedFeatures, hasFeatureType) + ).toBe(true); }); test('Is not single feature type when only supported feature type is not target feature type', () => { - expect(isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.LINE, supportedFeatures)).toBe(false); + expect( + isOnlySingleFeatureType(VECTOR_SHAPE_TYPE.LINE, supportedFeatures, hasFeatureType) + ).toBe(false); }); }); describe('source supports multiple feature types', () => { const supportedFeatures = [ - VECTOR_SHAPE_TYPES.POINT, - VECTOR_SHAPE_TYPES.LINE, - VECTOR_SHAPE_TYPES.POLYGON, + VECTOR_SHAPE_TYPE.POINT, + VECTOR_SHAPE_TYPE.LINE, + VECTOR_SHAPE_TYPE.POLYGON, ]; test('Is only single feature type when data only has target feature type', () => { const hasFeatureType = { - [VECTOR_SHAPE_TYPES.POINT]: true, - [VECTOR_SHAPE_TYPES.LINE]: false, - [VECTOR_SHAPE_TYPES.POLYGON]: false, + [VECTOR_SHAPE_TYPE.POINT]: true, + [VECTOR_SHAPE_TYPE.LINE]: false, + [VECTOR_SHAPE_TYPE.POLYGON]: false, }; expect( - isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT, supportedFeatures, hasFeatureType) + isOnlySingleFeatureType(VECTOR_SHAPE_TYPE.POINT, supportedFeatures, hasFeatureType) ).toBe(true); }); test('Is not single feature type when data has multiple feature types', () => { const hasFeatureType = { - [VECTOR_SHAPE_TYPES.POINT]: true, - [VECTOR_SHAPE_TYPES.LINE]: true, - [VECTOR_SHAPE_TYPES.POLYGON]: true, + [VECTOR_SHAPE_TYPE.POINT]: true, + [VECTOR_SHAPE_TYPE.LINE]: true, + [VECTOR_SHAPE_TYPE.POLYGON]: true, }; expect( - isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.LINE, supportedFeatures, hasFeatureType) + isOnlySingleFeatureType(VECTOR_SHAPE_TYPE.LINE, supportedFeatures, hasFeatureType) ).toBe(false); }); test('Is not single feature type when data does not have target feature types', () => { const hasFeatureType = { - [VECTOR_SHAPE_TYPES.POINT]: false, - [VECTOR_SHAPE_TYPES.LINE]: true, - [VECTOR_SHAPE_TYPES.POLYGON]: false, + [VECTOR_SHAPE_TYPE.POINT]: false, + [VECTOR_SHAPE_TYPE.LINE]: true, + [VECTOR_SHAPE_TYPE.POLYGON]: false, }; expect( - isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POINT, supportedFeatures, hasFeatureType) + isOnlySingleFeatureType(VECTOR_SHAPE_TYPE.POINT, supportedFeatures, hasFeatureType) ).toBe(false); }); }); @@ -64,7 +73,12 @@ describe('isOnlySingleFeatureType', () => { describe('assignCategoriesToPalette', () => { test('Categories and palette values have same length', () => { - const categories = [{ key: 'alpah' }, { key: 'bravo' }, { key: 'charlie' }, { key: 'delta' }]; + const categories = [ + { key: 'alpah', count: 1 }, + { key: 'bravo', count: 1 }, + { key: 'charlie', count: 1 }, + { key: 'delta', count: 1 }, + ]; const paletteValues = ['red', 'orange', 'yellow', 'green']; expect(assignCategoriesToPalette({ categories, paletteValues })).toEqual({ stops: [ @@ -72,31 +86,39 @@ describe('assignCategoriesToPalette', () => { { stop: 'bravo', style: 'orange' }, { stop: 'charlie', style: 'yellow' }, ], - fallback: 'green', + fallbackSymbolId: 'green', }); }); test('Should More categories than palette values', () => { - const categories = [{ key: 'alpah' }, { key: 'bravo' }, { key: 'charlie' }, { key: 'delta' }]; + const categories = [ + { key: 'alpah', count: 1 }, + { key: 'bravo', count: 1 }, + { key: 'charlie', count: 1 }, + { key: 'delta', count: 1 }, + ]; const paletteValues = ['red', 'orange', 'yellow']; expect(assignCategoriesToPalette({ categories, paletteValues })).toEqual({ stops: [ { stop: 'alpah', style: 'red' }, { stop: 'bravo', style: 'orange' }, ], - fallback: 'yellow', + fallbackSymbolId: 'yellow', }); }); test('Less categories than palette values', () => { - const categories = [{ key: 'alpah' }, { key: 'bravo' }]; + const categories = [ + { key: 'alpah', count: 1 }, + { key: 'bravo', count: 1 }, + ]; const paletteValues = ['red', 'orange', 'yellow', 'green', 'blue']; expect(assignCategoriesToPalette({ categories, paletteValues })).toEqual({ stops: [ { stop: 'alpah', style: 'red' }, { stop: 'bravo', style: 'orange' }, ], - fallback: 'yellow', + fallbackSymbolId: 'yellow', }); }); }); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_util.js b/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts similarity index 57% rename from x-pack/plugins/maps/public/classes/styles/vector/style_util.js rename to x-pack/plugins/maps/public/classes/styles/vector/style_util.ts index 3b62dcb27dced..d190a62e6f300 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/style_util.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts @@ -5,6 +5,8 @@ */ import { i18n } from '@kbn/i18n'; +import { MB_LOOKUP_FUNCTION, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; +import { Category } from '../../../../common/descriptor_types'; export function getOtherCategoryLabel() { return i18n.translate('xpack.maps.styles.categorical.otherCategoryLabel', { @@ -12,29 +14,32 @@ export function getOtherCategoryLabel() { }); } -export function getComputedFieldName(styleName, fieldName) { +export function getComputedFieldName(styleName: string, fieldName: string) { return `${getComputedFieldNamePrefix(fieldName)}__${styleName}`; } -export function getComputedFieldNamePrefix(fieldName) { +export function getComputedFieldNamePrefix(fieldName: string) { return `__kbn__dynamic__${fieldName}`; } -export function isOnlySingleFeatureType(featureType, supportedFeatures, hasFeatureType) { +export function isOnlySingleFeatureType( + featureType: VECTOR_SHAPE_TYPE, + supportedFeatures: VECTOR_SHAPE_TYPE[], + hasFeatureType: { [key in keyof typeof VECTOR_SHAPE_TYPE]: boolean } +): boolean { if (supportedFeatures.length === 1) { return supportedFeatures[0] === featureType; } const featureTypes = Object.keys(hasFeatureType); - return featureTypes.reduce((isOnlyTargetFeatureType, featureTypeKey) => { + // @ts-expect-error + return featureTypes.reduce((accumulator: boolean, featureTypeKey: VECTOR_SHAPE_TYPE) => { const hasFeature = hasFeatureType[featureTypeKey]; - return featureTypeKey === featureType - ? isOnlyTargetFeatureType && hasFeature - : isOnlyTargetFeatureType && !hasFeature; + return featureTypeKey === featureType ? accumulator && hasFeature : accumulator && !hasFeature; }, true); } -export function dynamicRound(value) { +export function dynamicRound(value: number | string) { if (typeof value !== 'number') { return value; } @@ -49,13 +54,19 @@ export function dynamicRound(value) { return precision === 0 ? Math.round(value) : parseFloat(value.toFixed(precision + 1)); } -export function assignCategoriesToPalette({ categories, paletteValues }) { +export function assignCategoriesToPalette({ + categories, + paletteValues, +}: { + categories: Category[]; + paletteValues: string[]; +}) { const stops = []; - let fallback = null; + let fallbackSymbolId = null; - if (categories && categories.length && paletteValues && paletteValues.length) { + if (categories.length && paletteValues.length) { const maxLength = Math.min(paletteValues.length, categories.length + 1); - fallback = paletteValues[maxLength - 1]; + fallbackSymbolId = paletteValues[maxLength - 1]; for (let i = 0; i < maxLength - 1; i++) { stops.push({ stop: categories[i].key, @@ -66,7 +77,7 @@ export function assignCategoriesToPalette({ categories, paletteValues }) { return { stops, - fallback, + fallbackSymbolId, }; } @@ -76,6 +87,12 @@ export function makeMbClampedNumberExpression({ minValue, maxValue, fallback, +}: { + lookupFunction: MB_LOOKUP_FUNCTION; + fieldName: string; + minValue: number; + maxValue: number; + fallback: number; }) { const clamp = ['max', ['min', ['to-number', [lookupFunction, fieldName]], maxValue], minValue]; return [ @@ -83,7 +100,7 @@ export function makeMbClampedNumberExpression({ [ 'case', ['==', [lookupFunction, fieldName], null], - minValue - 1, //== does a JS-y like check where returns true for null and undefined + minValue - 1, // == does a JS-y like check where returns true for null and undefined clamp, ], fallback, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js index 1672af8eccff8..04df9d73d75cd 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js @@ -140,5 +140,5 @@ export function getIconPaletteOptions(isDarkMode) { export function getIconPalette(paletteId) { const palette = ICON_PALETTES.find(({ id }) => id === paletteId); - return palette ? [...palette.icons] : null; + return palette ? [...palette.icons] : []; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js index 989ac268c0552..04a5381fa2592 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js @@ -16,12 +16,12 @@ import { SOURCE_FORMATTERS_DATA_REQUEST_ID, LAYER_STYLE_TYPE, DEFAULT_ICON, + VECTOR_SHAPE_TYPE, VECTOR_STYLES, } from '../../../../common/constants'; import { StyleMeta } from './style_meta'; import { VectorIcon } from './components/legend/vector_icon'; import { VectorStyleLegend } from './components/legend/vector_style_legend'; -import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; import { getComputedFieldName, isOnlySingleFeatureType } from './style_util'; import { StaticStyleProperty } from './properties/static_style_property'; import { DynamicStyleProperty } from './properties/dynamic_style_property'; @@ -249,24 +249,24 @@ export class VectorStyle extends AbstractStyle { const supportedFeatures = await this._source.getSupportedShapeTypes(); const hasFeatureType = { - [VECTOR_SHAPE_TYPES.POINT]: false, - [VECTOR_SHAPE_TYPES.LINE]: false, - [VECTOR_SHAPE_TYPES.POLYGON]: false, + [VECTOR_SHAPE_TYPE.POINT]: false, + [VECTOR_SHAPE_TYPE.LINE]: false, + [VECTOR_SHAPE_TYPE.POLYGON]: false, }; if (supportedFeatures.length > 1) { for (let i = 0; i < features.length; i++) { const feature = features[i]; - if (!hasFeatureType[VECTOR_SHAPE_TYPES.POINT] && POINTS.includes(feature.geometry.type)) { - hasFeatureType[VECTOR_SHAPE_TYPES.POINT] = true; + if (!hasFeatureType[VECTOR_SHAPE_TYPE.POINT] && POINTS.includes(feature.geometry.type)) { + hasFeatureType[VECTOR_SHAPE_TYPE.POINT] = true; } - if (!hasFeatureType[VECTOR_SHAPE_TYPES.LINE] && LINES.includes(feature.geometry.type)) { - hasFeatureType[VECTOR_SHAPE_TYPES.LINE] = true; + if (!hasFeatureType[VECTOR_SHAPE_TYPE.LINE] && LINES.includes(feature.geometry.type)) { + hasFeatureType[VECTOR_SHAPE_TYPE.LINE] = true; } if ( - !hasFeatureType[VECTOR_SHAPE_TYPES.POLYGON] && + !hasFeatureType[VECTOR_SHAPE_TYPE.POLYGON] && POLYGONS.includes(feature.geometry.type) ) { - hasFeatureType[VECTOR_SHAPE_TYPES.POLYGON] = true; + hasFeatureType[VECTOR_SHAPE_TYPE.POLYGON] = true; } } } @@ -274,17 +274,17 @@ export class VectorStyle extends AbstractStyle { const styleMeta = { geometryTypes: { isPointsOnly: isOnlySingleFeatureType( - VECTOR_SHAPE_TYPES.POINT, + VECTOR_SHAPE_TYPE.POINT, supportedFeatures, hasFeatureType ), isLinesOnly: isOnlySingleFeatureType( - VECTOR_SHAPE_TYPES.LINE, + VECTOR_SHAPE_TYPE.LINE, supportedFeatures, hasFeatureType ), isPolygonsOnly: isOnlySingleFeatureType( - VECTOR_SHAPE_TYPES.POLYGON, + VECTOR_SHAPE_TYPE.POLYGON, supportedFeatures, hasFeatureType ), diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js index 426f1d6afa952..a0dc07b8e545b 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js @@ -6,8 +6,7 @@ import { VectorStyle } from './vector_style'; import { DataRequest } from '../../util/data_request'; -import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; -import { FIELD_ORIGIN, STYLE_TYPE } from '../../../../common/constants'; +import { FIELD_ORIGIN, STYLE_TYPE, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; jest.mock('../../../kibana_services'); jest.mock('ui/new_platform'); @@ -28,7 +27,7 @@ class MockField { class MockSource { constructor({ supportedShapeTypes } = {}) { - this._supportedShapeTypes = supportedShapeTypes || Object.values(VECTOR_SHAPE_TYPES); + this._supportedShapeTypes = supportedShapeTypes || Object.values(VECTOR_SHAPE_TYPE); } getSupportedShapeTypes() { return this._supportedShapeTypes; From 57d64fb34b72b1510c7dbf5b3b75560c0d0b9e37 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 26 Jun 2020 06:45:24 -0600 Subject: [PATCH 77/82] [Maps] avoid using MAP_SAVED_OBJECT_TYPE constant when defining URL paths (#69723) (#70036) * [Maps] avoid using MAP_SAVED_OBJECT_TYPE constant when defining URL paths * rename methods Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- x-pack/plugins/maps/common/constants.ts | 11 +++++++---- .../maps/public/embeddable/map_embeddable_factory.ts | 4 ++-- x-pack/plugins/maps/public/maps_vis_type_alias.js | 4 ++-- .../routing/bootstrap/services/saved_gis_map.js | 4 ++-- .../maps/public/routing/page_elements/breadcrumbs.js | 4 ++-- x-pack/plugins/maps/server/plugin.ts | 8 ++++---- x-pack/plugins/maps/server/saved_objects/map.ts | 4 ++-- x-pack/plugins/maps/server/tutorials/ems/index.ts | 4 ++-- 8 files changed, 23 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index bf30006441c9d..f7374ba91f8fe 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -26,14 +26,17 @@ export const MAP_SAVED_OBJECT_TYPE = 'map'; export const APP_ID = 'maps'; export const APP_ICON = 'gisApp'; -export const MAP_APP_PATH = `app/${APP_ID}`; +export const MAPS_APP_PATH = `app/${APP_ID}`; +export const MAP_PATH = 'map'; export const GIS_API_PATH = `api/${APP_ID}`; export const INDEX_SETTINGS_API_PATH = `${GIS_API_PATH}/indexSettings`; export const FONTS_API_PATH = `${GIS_API_PATH}/fonts`; -export const MAP_BASE_URL = `/${MAP_APP_PATH}/${MAP_SAVED_OBJECT_TYPE}`; - -export function createMapPath(id: string) { +const MAP_BASE_URL = `/${MAPS_APP_PATH}/${MAP_PATH}`; +export function getNewMapPath() { + return MAP_BASE_URL; +} +export function getExistingMapPath(id: string) { return `${MAP_BASE_URL}/${id}`; } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index c73225fc4285b..8fb0ecb50b28b 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -12,7 +12,7 @@ import { IContainer, } from '../../../../../src/plugins/embeddable/public'; import '../index.scss'; -import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; +import { getExistingMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; import { LayerDescriptor } from '../../common/descriptor_types'; import { MapEmbeddableInput } from './types'; import { lazyLoadMapModules } from '../lazy_load_bundle'; @@ -113,7 +113,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { { layerList, title: savedMap.title, - editUrl: getHttp().basePath.prepend(createMapPath(savedObjectId)), + editUrl: getHttp().basePath.prepend(getExistingMapPath(savedObjectId)), indexPatterns, editable: await this.isEditable(), settings, diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.js b/x-pack/plugins/maps/public/maps_vis_type_alias.js index cb7b3db17eab5..d90674f0f7725 100644 --- a/x-pack/plugins/maps/public/maps_vis_type_alias.js +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.js @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +import { APP_ID, APP_ICON, MAP_PATH } from '../common/constants'; import { getShowMapVisualizationTypes, getVisualizations } from './kibana_services'; export function getMapsVisTypeAlias() { @@ -28,7 +28,7 @@ The Maps app offers more functionality and is easier to use.`, return { aliasApp: APP_ID, - aliasPath: `/${MAP_SAVED_OBJECT_TYPE}`, + aliasPath: `/${MAP_PATH}`, name: APP_ID, title: i18n.translate('xpack.maps.visTypeAlias.title', { defaultMessage: 'Maps', diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js index f24c7be65afa3..f8c783f673bab 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.js @@ -19,7 +19,7 @@ import { import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../../selectors/ui_selectors'; import { copyPersistentState } from '../../../reducers/util'; import { extractReferences, injectReferences } from '../../../../common/migrations/references'; -import { MAP_BASE_URL, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +import { getExistingMapPath, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; import { getStore } from '../../store_operations'; export function createSavedGisMapClass(services) { @@ -76,7 +76,7 @@ export function createSavedGisMapClass(services) { } getFullPath() { - return `${MAP_BASE_URL}/${this.id}`; + return getExistingMapPath(this.id); } getLayerList() { diff --git a/x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js b/x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js index 36a355719d945..de2ee42b49171 100644 --- a/x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js +++ b/x-pack/plugins/maps/public/routing/page_elements/breadcrumbs.js @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { getCoreChrome } from '../../kibana_services'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { MAP_PATH } from '../../../common/constants'; import _ from 'lodash'; import { getLayerListRaw } from '../../selectors/map_selectors'; import { copyPersistentState } from '../../reducers/util'; @@ -31,7 +31,7 @@ function hasUnsavedChanges(savedMap, initialLayerListConfig) { } export const updateBreadcrumbs = (savedMap, initialLayerListConfig, currentPath = '') => { - const isOnMapNow = currentPath.startsWith(`/${MAP_SAVED_OBJECT_TYPE}`); + const isOnMapNow = currentPath.startsWith(`/${MAP_PATH}`); const breadCrumbs = isOnMapNow ? [ { diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index fe2b73df7978f..60f3a9b68202c 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -14,7 +14,7 @@ import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js'; // @ts-ignore import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js'; import { registerMapsUsageCollector } from './maps_telemetry/collectors/register'; -import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, createMapPath } from '../common/constants'; +import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../common/constants'; import { mapSavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore @@ -58,7 +58,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addAppLinksToSampleDataset('ecommerce', [ { - path: createMapPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'), + path: getExistingMapPath('2c9c1f60-1909-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, @@ -80,7 +80,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addAppLinksToSampleDataset('flights', [ { - path: createMapPath('5dd88580-1906-11e9-919b-ffe5949a18d2'), + path: getExistingMapPath('5dd88580-1906-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, @@ -101,7 +101,7 @@ export class MapsPlugin implements Plugin { home.sampleData.addSavedObjectsToSampleDataset('logs', getWebLogsSavedObjects()); home.sampleData.addAppLinksToSampleDataset('logs', [ { - path: createMapPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'), + path: getExistingMapPath('de71f4f0-1902-11e9-919b-ffe5949a18d2'), label: sampleDataLinkLabel, icon: APP_ICON, }, diff --git a/x-pack/plugins/maps/server/saved_objects/map.ts b/x-pack/plugins/maps/server/saved_objects/map.ts index 0fcadc5a97203..ce9d579137864 100644 --- a/x-pack/plugins/maps/server/saved_objects/map.ts +++ b/x-pack/plugins/maps/server/saved_objects/map.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsType } from 'src/core/server'; -import { APP_ICON, createMapPath } from '../../common/constants'; +import { APP_ICON, getExistingMapPath } from '../../common/constants'; // @ts-ignore import { migrations } from './migrations'; @@ -31,7 +31,7 @@ export const mapSavedObjects: SavedObjectsType = { }, getInAppUrl(obj) { return { - path: createMapPath(obj.id), + path: getExistingMapPath(obj.id), uiCapabilitiesPath: 'maps.show', }; }, diff --git a/x-pack/plugins/maps/server/tutorials/ems/index.ts b/x-pack/plugins/maps/server/tutorials/ems/index.ts index e96af89e52685..be15120cb19e1 100644 --- a/x-pack/plugins/maps/server/tutorials/ems/index.ts +++ b/x-pack/plugins/maps/server/tutorials/ems/index.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { TutorialsCategory } from '../../../../../../src/plugins/home/server'; -import { MAP_BASE_URL } from '../../../common/constants'; +import { getNewMapPath } from '../../../common/constants'; export function emsBoundariesSpecProvider({ emsLandingPageUrl, @@ -64,7 +64,7 @@ Indexing EMS administrative boundaries in Elasticsearch allows for search on bou 2. Click `Add layer`, then select `Upload GeoJSON`.\n\ 3. Upload the GeoJSON file and click `Import file`.', values: { - newMapUrl: prependBasePath(MAP_BASE_URL), + newMapUrl: prependBasePath(getNewMapPath()), }, }), }, From 61063890d8768999988637a3f0331d741536aa7b Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 26 Jun 2020 15:09:20 +0200 Subject: [PATCH 78/82] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20fix=20typo=20i?= =?UTF-8?q?n=20embeddable=20(#69417)=20(#70053)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../embeddable/public/lib/embeddables/embeddable.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx index 9c544e86e189a..fcecf117d7d52 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable.tsx @@ -51,8 +51,7 @@ export abstract class Embeddable< // to update input when the parent changes. private parentSubscription?: Rx.Subscription; - // TODO: Rename to destroyed. - private destoyed: boolean = false; + private destroyed: boolean = false; constructor(input: TEmbeddableInput, output: TEmbeddableOutput, parent?: IContainer) { this.id = input.id; @@ -123,7 +122,7 @@ export abstract class Embeddable< } public updateInput(changes: Partial): void { - if (this.destoyed) { + if (this.destroyed) { throw new Error('Embeddable has been destroyed'); } if (this.parent) { @@ -135,7 +134,7 @@ export abstract class Embeddable< } public render(domNode: HTMLElement | Element): void { - if (this.destoyed) { + if (this.destroyed) { throw new Error('Embeddable has been destroyed'); } return; @@ -155,7 +154,7 @@ export abstract class Embeddable< * implementors to add any additional clean up tasks, like unmounting and unsubscribing. */ public destroy(): void { - this.destoyed = true; + this.destroyed = true; this.input$.complete(); this.output$.complete(); From a5b6788226ddda4c4b12fdf3197c822c9c2c8b34 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Fri, 26 Jun 2020 15:47:43 +0200 Subject: [PATCH 79/82] Api reference docs for state_containers and state_sync (#67354) (#70058) Adds state_containers and state_sync to api_extractor improves TSDoc definitions for those plugins adds changes to api_extractor script to support common/ folder and runs docs generation sequentially to not get OOM. Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../common/state_containers/index.md | 12 ++ ...utils-common-state_containers.basestate.md | 13 ++ ...state_containers.basestatecontainer.get.md | 13 ++ ...mon-state_containers.basestatecontainer.md | 22 +++ ...state_containers.basestatecontainer.set.md | 13 ++ ...te_containers.basestatecontainer.state_.md | 13 ++ ...tils-common-state_containers.comparator.md | 13 ++ ...a_utils-common-state_containers.connect.md | 13 ++ ...n-state_containers.createstatecontainer.md | 24 +++ ...state_containers.createstatecontainer_1.md | 25 +++ ...state_containers.createstatecontainer_2.md | 27 +++ ...ners.createstatecontaineroptions.freeze.md | 25 +++ ..._containers.createstatecontaineroptions.md | 20 +++ ...ainers.createstatecontainerreacthelpers.md | 22 +++ ..._utils-common-state_containers.dispatch.md | 13 ++ ...mon-state_containers.ensurepureselector.md | 12 ++ ...n-state_containers.ensurepuretransition.md | 12 ++ ...common-state_containers.mapstatetoprops.md | 13 ++ ...ns-kibana_utils-common-state_containers.md | 52 ++++++ ...tils-common-state_containers.middleware.md | 13 ++ ...ls-common-state_containers.pureselector.md | 12 ++ ...ate_containers.pureselectorstoselectors.md | 14 ++ ...state_containers.pureselectortoselector.md | 12 ++ ...a_utils-common-state_containers.reducer.md | 13 ++ ...s.reduxlikestatecontainer.addmiddleware.md | 11 ++ ...ainers.reduxlikestatecontainer.dispatch.md | 11 ++ ...ainers.reduxlikestatecontainer.getstate.md | 11 ++ ...tate_containers.reduxlikestatecontainer.md | 25 +++ ...tainers.reduxlikestatecontainer.reducer.md | 11 ++ ....reduxlikestatecontainer.replacereducer.md | 11 ++ ...iners.reduxlikestatecontainer.subscribe.md | 11 ++ ..._utils-common-state_containers.selector.md | 12 ++ ...-common-state_containers.statecontainer.md | 21 +++ ...ate_containers.statecontainer.selectors.md | 11 ++ ...e_containers.statecontainer.transitions.md | 11 ++ ...tils-common-state_containers.unboxstate.md | 13 ++ ...n-state_containers.usecontainerselector.md | 13 ++ ...mmon-state_containers.usecontainerstate.md | 13 ++ .../kibana_utils/public/state_sync/index.md | 12 ++ ...lic-state_sync.createkbnurlstatestorage.md | 16 ++ ...e_sync.createsessionstoragestatestorage.md | 13 ++ ...c-state_sync.ikbnurlstatestorage.cancel.md | 13 ++ ...-state_sync.ikbnurlstatestorage.change_.md | 11 ++ ...ic-state_sync.ikbnurlstatestorage.flush.md | 15 ++ ...blic-state_sync.ikbnurlstatestorage.get.md | 11 ++ ...s-public-state_sync.ikbnurlstatestorage.md | 24 +++ ...blic-state_sync.ikbnurlstatestorage.set.md | 13 ++ ...-state_sync.inullablebasestatecontainer.md | 24 +++ ...te_sync.inullablebasestatecontainer.set.md | 11 ++ ...te_sync.isessionstoragestatestorage.get.md | 11 ++ ...-state_sync.isessionstoragestatestorage.md | 21 +++ ...te_sync.isessionstoragestatestorage.set.md | 11 ++ ...-public-state_sync.istatestorage.cancel.md | 13 ++ ...public-state_sync.istatestorage.change_.md | 13 ++ ...ils-public-state_sync.istatestorage.get.md | 13 ++ ...a_utils-public-state_sync.istatestorage.md | 25 +++ ...ils-public-state_sync.istatestorage.set.md | 13 ++ ...tils-public-state_sync.istatesyncconfig.md | 22 +++ ...te_sync.istatesyncconfig.statecontainer.md | 13 ++ ...tate_sync.istatesyncconfig.statestorage.md | 15 ++ ...-state_sync.istatesyncconfig.storagekey.md | 13 ++ ...a_utils-public-state_sync.isyncstateref.md | 20 +++ ...s-public-state_sync.isyncstateref.start.md | 13 ++ ...ls-public-state_sync.isyncstateref.stop.md | 13 ++ ...-plugins-kibana_utils-public-state_sync.md | 48 ++++++ ...-public-state_sync.startsyncstatefntype.md | 12 ++ ...s-public-state_sync.stopsyncstatefntype.md | 12 ++ ...ibana_utils-public-state_sync.syncstate.md | 93 +++++++++++ ...bana_utils-public-state_sync.syncstates.md | 42 +++++ src/dev/run_check_published_api_changes.ts | 56 +++++-- .../common/state_containers/common.api.md | 156 ++++++++++++++++++ .../create_state_container.ts | 43 ++++- .../create_state_container_react_helpers.ts | 23 ++- .../common/state_containers/index.ts | 40 ++++- .../common/state_containers/types.ts | 106 +++++++++++- .../kibana_utils/public/state_sync/index.ts | 21 +++ .../public/state_sync/public.api.md | 97 +++++++++++ .../public/state_sync/state_sync.ts | 68 +++++--- .../create_kbn_url_state_storage.ts | 30 +++- .../create_session_storage_state_storage.ts | 12 ++ .../state_sync_state_storage/types.ts | 5 +- .../kibana_utils/public/state_sync/types.ts | 25 ++- 82 files changed, 1816 insertions(+), 71 deletions(-) create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/index.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md create mode 100644 docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/index.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md create mode 100644 docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md create mode 100644 src/plugins/kibana_utils/common/state_containers/common.api.md create mode 100644 src/plugins/kibana_utils/public/state_sync/public.api.md diff --git a/docs/development/plugins/kibana_utils/common/state_containers/index.md b/docs/development/plugins/kibana_utils/common/state_containers/index.md new file mode 100644 index 0000000000000..b4e1071ceb732 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/index.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) + +## API Reference + +## Packages + +| Package | Description | +| --- | --- | +| [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) | State containers are Redux-store-like objects meant to help you manage state in your services or apps. Refer to [guides and examples](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers) for more info | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md new file mode 100644 index 0000000000000..92893afc02bef --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseState](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md) + +## BaseState type + +Base [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) state shape + +Signature: + +```typescript +export declare type BaseState = object; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md new file mode 100644 index 0000000000000..b939954d92aa6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) > [get](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md) + +## BaseStateContainer.get property + +Retrieves current state from the container + +Signature: + +```typescript +get: () => State; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md new file mode 100644 index 0000000000000..66c25c87f5e37 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) + +## BaseStateContainer interface + +Base state container shape without transitions or selectors + +Signature: + +```typescript +export interface BaseStateContainer +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [get](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.get.md) | () => State | Retrieves current state from the container | +| [set](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md) | (state: State) => void | Sets state into container | +| [state$](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md) | Observable<State> | of state | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md new file mode 100644 index 0000000000000..ed4ff365adfb3 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) > [set](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.set.md) + +## BaseStateContainer.set property + +Sets state into container + +Signature: + +```typescript +set: (state: State) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md new file mode 100644 index 0000000000000..35838fa53d539 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) > [state$](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.state_.md) + +## BaseStateContainer.state$ property + + of state + +Signature: + +```typescript +state$: Observable; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md new file mode 100644 index 0000000000000..12af33756fb19 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Comparator](./kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md) + +## Comparator type + +Used to compare state. see [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) + +Signature: + +```typescript +export declare type Comparator = (previous: Result, current: Result) => boolean; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md new file mode 100644 index 0000000000000..e05f1fb392fe6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) + +## Connect type + +Similar to `connect` from react-redux, allows to map state from state container to component's props + +Signature: + +```typescript +export declare type Connect = (mapStateToProp: MapStateToProps>) => (component: ComponentType) => FC>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md new file mode 100644 index 0000000000000..cc43b59676dc1 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [createStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md) + +## createStateContainer() function + +Creates a state container without transitions and without selectors. + +Signature: + +```typescript +export declare function createStateContainer(defaultState: State): ReduxLikeStateContainer; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| defaultState | State | initial state | + +Returns: + +`ReduxLikeStateContainer` + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md new file mode 100644 index 0000000000000..794bf63588312 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [createStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md) + +## createStateContainer() function + +Creates a state container with transitions, but without selectors + +Signature: + +```typescript +export declare function createStateContainer(defaultState: State, pureTransitions: PureTransitions): ReduxLikeStateContainer; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| defaultState | State | initial state | +| pureTransitions | PureTransitions | state transitions configuration object. Map of . | + +Returns: + +`ReduxLikeStateContainer` + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md new file mode 100644 index 0000000000000..1946baae202f1 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [createStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md) + +## createStateContainer() function + +Creates a state container with transitions and selectors + +Signature: + +```typescript +export declare function createStateContainer(defaultState: State, pureTransitions: PureTransitions, pureSelectors: PureSelectors, options?: CreateStateContainerOptions): ReduxLikeStateContainer; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| defaultState | State | initial state | +| pureTransitions | PureTransitions | state transitions configuration object. Map of . | +| pureSelectors | PureSelectors | state selectors configuration object. Map of . | +| options | CreateStateContainerOptions | state container options [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) | + +Returns: + +`ReduxLikeStateContainer` + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md new file mode 100644 index 0000000000000..4f772c7c54d08 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) > [freeze](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md) + +## CreateStateContainerOptions.freeze property + +Function to use when freezing state. Supply identity function. If not provided, default deepFreeze is use. + +Signature: + +```typescript +freeze?: (state: T) => T; +``` + +## Example + +If you expect that your state will be mutated externally an you cannot prevent that + +```ts +{ + freeze: state => state, +} + +``` + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md new file mode 100644 index 0000000000000..d328d306e93e1 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) + +## CreateStateContainerOptions interface + +State container options + +Signature: + +```typescript +export interface CreateStateContainerOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [freeze](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.freeze.md) | <T>(state: T) => T | Function to use when freezing state. Supply identity function. If not provided, default deepFreeze is use. | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md new file mode 100644 index 0000000000000..a6076490c2746 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [createStateContainerReactHelpers](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md) + +## createStateContainerReactHelpers variable + +Creates helpers for using [State Containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) with react Refer to [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_containers/react.md) for details + +Signature: + +```typescript +createStateContainerReactHelpers: >() => { + Provider: React.Provider; + Consumer: React.Consumer; + context: React.Context; + useContainer: () => Container; + useState: () => UnboxState; + useTransitions: () => Container["transitions"]; + useSelector: (selector: (state: UnboxState) => Result, comparator?: Comparator) => Result; + connect: Connect>; +} +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md new file mode 100644 index 0000000000000..d4057a549bb0d --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md) + +## Dispatch type + +Redux like dispatch + +Signature: + +```typescript +export declare type Dispatch = (action: T) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md new file mode 100644 index 0000000000000..5e4e86ad82d53 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [EnsurePureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md) + +## EnsurePureSelector type + + +Signature: + +```typescript +export declare type EnsurePureSelector = Ensure>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md new file mode 100644 index 0000000000000..0e621e989346b --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [EnsurePureTransition](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md) + +## EnsurePureTransition type + + +Signature: + +```typescript +export declare type EnsurePureTransition = Ensure>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md new file mode 100644 index 0000000000000..8e6a49ac72742 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [MapStateToProps](./kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md) + +## MapStateToProps type + +State container state to component props mapper. See [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) + +Signature: + +```typescript +export declare type MapStateToProps = (state: State) => StateProps; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md new file mode 100644 index 0000000000000..e74ff2c6885be --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.md @@ -0,0 +1,52 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) + +## kibana-plugin-plugins-kibana\_utils-common-state\_containers package + +State containers are Redux-store-like objects meant to help you manage state in your services or apps. Refer to [guides and examples](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers) for more info + +## Functions + +| Function | Description | +| --- | --- | +| [createStateContainer(defaultState)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer.md) | Creates a state container without transitions and without selectors. | +| [createStateContainer(defaultState, pureTransitions)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_1.md) | Creates a state container with transitions, but without selectors | +| [createStateContainer(defaultState, pureTransitions, pureSelectors, options)](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainer_2.md) | Creates a state container with transitions and selectors | + +## Interfaces + +| Interface | Description | +| --- | --- | +| [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) | Base state container shape without transitions or selectors | +| [CreateStateContainerOptions](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontaineroptions.md) | State container options | +| [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) | Fully featured state container which matches Redux store interface. Extends [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) Allows to use state container with redux libraries | +| [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) | Fully featured state container with [Selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) and . Extends [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) | + +## Variables + +| Variable | Description | +| --- | --- | +| [createStateContainerReactHelpers](./kibana-plugin-plugins-kibana_utils-common-state_containers.createstatecontainerreacthelpers.md) | Creates helpers for using [State Containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) with react Refer to [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_containers/react.md) for details | +| [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) | React hook to apply selector to state container to extract only needed information. Will re-render your component only when the section changes. | +| [useContainerState](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md) | React hooks that returns the latest state of a [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md). | + +## Type Aliases + +| Type Alias | Description | +| --- | --- | +| [BaseState](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestate.md) | Base [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) state shape | +| [Comparator](./kibana-plugin-plugins-kibana_utils-common-state_containers.comparator.md) | Used to compare state. see [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) | +| [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) | Similar to connect from react-redux, allows to map state from state container to component's props | +| [Dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.dispatch.md) | Redux like dispatch | +| [EnsurePureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepureselector.md) | | +| [EnsurePureTransition](./kibana-plugin-plugins-kibana_utils-common-state_containers.ensurepuretransition.md) | | +| [MapStateToProps](./kibana-plugin-plugins-kibana_utils-common-state_containers.mapstatetoprops.md) | State container state to component props mapper. See [Connect](./kibana-plugin-plugins-kibana_utils-common-state_containers.connect.md) | +| [Middleware](./kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md) | Redux like Middleware | +| [PureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md) | | +| [PureSelectorsToSelectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md) | | +| [PureSelectorToSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md) | | +| [Reducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md) | Redux like Reducer | +| [Selector](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) | | +| [UnboxState](./kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md) | Utility type for inferring state shape from [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md new file mode 100644 index 0000000000000..574b83306dc95 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Middleware](./kibana-plugin-plugins-kibana_utils-common-state_containers.middleware.md) + +## Middleware type + +Redux like Middleware + +Signature: + +```typescript +export declare type Middleware = (store: Pick, 'getState' | 'dispatch'>) => (next: (action: TransitionDescription) => TransitionDescription | any) => Dispatch; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md new file mode 100644 index 0000000000000..6ac07cba446f5 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [PureSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselector.md) + +## PureSelector type + + +Signature: + +```typescript +export declare type PureSelector = (state: State) => Selector; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md new file mode 100644 index 0000000000000..82a91f7c87e17 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [PureSelectorsToSelectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectorstoselectors.md) + +## PureSelectorsToSelectors type + + +Signature: + +```typescript +export declare type PureSelectorsToSelectors = { + [K in keyof T]: PureSelectorToSelector>; +}; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md new file mode 100644 index 0000000000000..5c12afd1cd971 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [PureSelectorToSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.pureselectortoselector.md) + +## PureSelectorToSelector type + + +Signature: + +```typescript +export declare type PureSelectorToSelector> = ReturnType>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md new file mode 100644 index 0000000000000..519e6ce7d7cfb --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Reducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reducer.md) + +## Reducer type + +Redux like Reducer + +Signature: + +```typescript +export declare type Reducer = (state: State, action: TransitionDescription) => State; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md new file mode 100644 index 0000000000000..e90da05e30d87 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [addMiddleware](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md) + +## ReduxLikeStateContainer.addMiddleware property + +Signature: + +```typescript +addMiddleware: (middleware: Middleware) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md new file mode 100644 index 0000000000000..7a9755ee3b65c --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md) + +## ReduxLikeStateContainer.dispatch property + +Signature: + +```typescript +dispatch: (action: TransitionDescription) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md new file mode 100644 index 0000000000000..86e1c6dd34cd6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [getState](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md) + +## ReduxLikeStateContainer.getState property + +Signature: + +```typescript +getState: () => State; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md new file mode 100644 index 0000000000000..0e08119c1eae4 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) + +## ReduxLikeStateContainer interface + +Fully featured state container which matches Redux store interface. Extends [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) Allows to use state container with redux libraries + +Signature: + +```typescript +export interface ReduxLikeStateContainer extends StateContainer +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [addMiddleware](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.addmiddleware.md) | (middleware: Middleware<State>) => void | | +| [dispatch](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.dispatch.md) | (action: TransitionDescription) => void | | +| [getState](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.getstate.md) | () => State | | +| [reducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md) | Reducer<State> | | +| [replaceReducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md) | (nextReducer: Reducer<State>) => void | | +| [subscribe](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md) | (listener: (state: State) => void) => () => void | | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md new file mode 100644 index 0000000000000..49eabf19340f2 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [reducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.reducer.md) + +## ReduxLikeStateContainer.reducer property + +Signature: + +```typescript +reducer: Reducer; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md new file mode 100644 index 0000000000000..2582d31d9adc4 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [replaceReducer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.replacereducer.md) + +## ReduxLikeStateContainer.replaceReducer property + +Signature: + +```typescript +replaceReducer: (nextReducer: Reducer) => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md new file mode 100644 index 0000000000000..15139a7bd9f3e --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [ReduxLikeStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.md) > [subscribe](./kibana-plugin-plugins-kibana_utils-common-state_containers.reduxlikestatecontainer.subscribe.md) + +## ReduxLikeStateContainer.subscribe property + +Signature: + +```typescript +subscribe: (listener: (state: State) => void) => () => void; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md new file mode 100644 index 0000000000000..5c143551d130b --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [Selector](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) + +## Selector type + + +Signature: + +```typescript +export declare type Selector = (...args: Args) => Result; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md new file mode 100644 index 0000000000000..23ec1c8e5be01 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) + +## StateContainer interface + +Fully featured state container with [Selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.selector.md) and . Extends [BaseStateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.basestatecontainer.md) + +Signature: + +```typescript +export interface StateContainer extends BaseStateContainer +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md) | Readonly<PureSelectorsToSelectors<PureSelectors>> | | +| [transitions](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md) | Readonly<PureTransitionsToTransitions<PureTransitions>> | | + diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md new file mode 100644 index 0000000000000..2afac07b59e39 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) > [selectors](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.selectors.md) + +## StateContainer.selectors property + +Signature: + +```typescript +selectors: Readonly>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md new file mode 100644 index 0000000000000..4712d3287beef --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) > [transitions](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.transitions.md) + +## StateContainer.transitions property + +Signature: + +```typescript +transitions: Readonly>; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md new file mode 100644 index 0000000000000..d4f99841456d7 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [UnboxState](./kibana-plugin-plugins-kibana_utils-common-state_containers.unboxstate.md) + +## UnboxState type + +Utility type for inferring state shape from [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md) + +Signature: + +```typescript +export declare type UnboxState> = Container extends StateContainer ? T : never; +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md new file mode 100644 index 0000000000000..fe5f30a9c8472 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [useContainerSelector](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerselector.md) + +## useContainerSelector variable + +React hook to apply selector to state container to extract only needed information. Will re-render your component only when the section changes. + +Signature: + +```typescript +useContainerSelector: , Result>(container: Container, selector: (state: UnboxState) => Result, comparator?: Comparator) => Result +``` diff --git a/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md new file mode 100644 index 0000000000000..7cef47c58f9d9 --- /dev/null +++ b/docs/development/plugins/kibana_utils/common/state_containers/kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-common-state\_containers](./kibana-plugin-plugins-kibana_utils-common-state_containers.md) > [useContainerState](./kibana-plugin-plugins-kibana_utils-common-state_containers.usecontainerstate.md) + +## useContainerState variable + +React hooks that returns the latest state of a [StateContainer](./kibana-plugin-plugins-kibana_utils-common-state_containers.statecontainer.md). + +Signature: + +```typescript +useContainerState: >(container: Container) => UnboxState +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/index.md b/docs/development/plugins/kibana_utils/public/state_sync/index.md new file mode 100644 index 0000000000000..4b345d9130bd5 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/index.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) + +## API Reference + +## Packages + +| Package | Description | +| --- | --- | +| [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) | State syncing utilities are a set of helpers for syncing your application state with URL or browser storage.They are designed to work together with state containers (). But state containers are not required.State syncing utilities include:- util which: - Subscribes to state changes and pushes them to state storage. - Optionally subscribes to state storage changes and pushes them to state. - Two types of storage compatible with syncState: - - Serializes state and persists it to URL's query param in rison or hashed format. Listens for state updates in the URL and pushes them back to state. - - Serializes state and persists it to browser storage.Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md new file mode 100644 index 0000000000000..22f70ce22b574 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [createKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md) + +## createKbnUrlStateStorage variable + +Creates [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) state storage + +Signature: + +```typescript +createKbnUrlStateStorage: ({ useHash, history }?: { + useHash: boolean; + history?: History | undefined; +}) => IKbnUrlStateStorage +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md new file mode 100644 index 0000000000000..dccff93ad1724 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [createSessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md) + +## createSessionStorageStateStorage variable + +Creates [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) + +Signature: + +```typescript +createSessionStorageStateStorage: (storage?: Storage) => ISessionStorageStateStorage +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md new file mode 100644 index 0000000000000..29a511d57d7bd --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md) + +## IKbnUrlStateStorage.cancel property + +cancels any pending url updates + +Signature: + +```typescript +cancel: () => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md new file mode 100644 index 0000000000000..2b55f2aca70c8 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md) + +## IKbnUrlStateStorage.change$ property + +Signature: + +```typescript +change$: (key: string) => Observable; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md new file mode 100644 index 0000000000000..e0e6aa9be4368 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [flush](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md) + +## IKbnUrlStateStorage.flush property + +synchronously runs any pending url updates returned boolean indicates if change occurred + +Signature: + +```typescript +flush: (opts?: { + replace?: boolean; + }) => boolean; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md new file mode 100644 index 0000000000000..0eb60c21fbbbf --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md) + +## IKbnUrlStateStorage.get property + +Signature: + +```typescript +get: (key: string) => State | null; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md new file mode 100644 index 0000000000000..56cefebd2acfe --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) + +## IKbnUrlStateStorage interface + +KbnUrlStateStorage is a state storage for [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) utility which: 1. Keeps state in sync with the URL. 2. Serializes data and stores it in the URL in one of the supported formats: \* Rison encoded. \* Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See kibana's advanced option for more context state:storeInSessionStorage 3. Takes care of listening to the URL updates and notifies state about the updates. 4. Takes care of batching URL updates to prevent redundant browser history records. [GUIDE](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md) + +Signature: + +```typescript +export interface IKbnUrlStateStorage extends IStateStorage +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.cancel.md) | () => void | cancels any pending url updates | +| [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.change_.md) | <State = unknown>(key: string) => Observable<State | null> | | +| [flush](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.flush.md) | (opts?: {
replace?: boolean;
}) => boolean | synchronously runs any pending url updates returned boolean indicates if change occurred | +| [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.get.md) | <State = unknown>(key: string) => State | null | | +| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md) | <State>(key: string, state: State, opts?: {
replace: boolean;
}) => Promise<string | undefined> | | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md new file mode 100644 index 0000000000000..2eab44d344414 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) > [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.set.md) + +## IKbnUrlStateStorage.set property + +Signature: + +```typescript +set: (key: string, state: State, opts?: { + replace: boolean; + }) => Promise; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md new file mode 100644 index 0000000000000..ca69609936405 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) + +## INullableBaseStateContainer interface + +Extension of with one constraint: set state should handle `null` as incoming state + +Signature: + +```typescript +export interface INullableBaseStateContainer extends BaseStateContainer +``` + +## Remarks + +State container for stateSync() have to accept "null" for example, set() implementation could handle null and fallback to some default state this is required to handle edge case, when state in storage becomes empty and syncing is in progress. state container will be notified about about storage becoming empty with null passed in + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md) | (state: State | null) => void | | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md new file mode 100644 index 0000000000000..dd2978f59484a --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) > [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.set.md) + +## INullableBaseStateContainer.set property + +Signature: + +```typescript +set: (state: State | null) => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md new file mode 100644 index 0000000000000..83131c77132ce --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) > [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md) + +## ISessionStorageStateStorage.get property + +Signature: + +```typescript +get: (key: string) => State | null; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md new file mode 100644 index 0000000000000..7792bc3932f95 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) + +## ISessionStorageStateStorage interface + +[IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) for storing state in browser [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) + +Signature: + +```typescript +export interface ISessionStorageStateStorage extends IStateStorage +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.get.md) | <State = unknown>(key: string) => State | null | | +| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md) | <State>(key: string, state: State) => void | | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md new file mode 100644 index 0000000000000..04b0ab01f0d13 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) > [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.set.md) + +## ISessionStorageStateStorage.set property + +Signature: + +```typescript +set: (key: string, state: State) => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md new file mode 100644 index 0000000000000..ce771d52a6e60 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) > [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md) + +## IStateStorage.cancel property + +Optional method to cancel any pending activity syncState() will call it, if it is provided by IStateStorage + +Signature: + +```typescript +cancel?: () => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md new file mode 100644 index 0000000000000..ed6672a3d83c6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) > [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md) + +## IStateStorage.change$ property + +Should notify when the stored state has changed + +Signature: + +```typescript +change$?: (key: string) => Observable; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md new file mode 100644 index 0000000000000..2c0b2ee970cc6 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) > [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md) + +## IStateStorage.get property + +Should retrieve state from the storage and deserialize it + +Signature: + +```typescript +get: (key: string) => State | null; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md new file mode 100644 index 0000000000000..2c34a185fb7b1 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) + +## IStateStorage interface + +Any StateStorage have to implement IStateStorage interface StateStorage is responsible for: \* state serialisation / deserialization \* persisting to and retrieving from storage + +For an example take a look at already implemented [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) and [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) state storages + +Signature: + +```typescript +export interface IStateStorage +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [cancel](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.cancel.md) | () => void | Optional method to cancel any pending activity syncState() will call it, if it is provided by IStateStorage | +| [change$](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.change_.md) | <State = unknown>(key: string) => Observable<State | null> | Should notify when the stored state has changed | +| [get](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.get.md) | <State = unknown>(key: string) => State | null | Should retrieve state from the storage and deserialize it | +| [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md) | <State>(key: string, state: State) => any | Take in a state object, should serialise and persist | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md new file mode 100644 index 0000000000000..3f286994ed4af --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) > [set](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.set.md) + +## IStateStorage.set property + +Take in a state object, should serialise and persist + +Signature: + +```typescript +set: (key: string, state: State) => any; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md new file mode 100644 index 0000000000000..f9368de4240ac --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) + +## IStateSyncConfig interface + +Config for setting up state syncing with + +Signature: + +```typescript +export interface IStateSyncConfig +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [stateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md) | INullableBaseStateContainer<State> | State container to keep in sync with storage, have to implement [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) interface We encourage to use as a state container, but it is also possible to implement own custom container for advanced use cases | +| [stateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md) | StateStorage | State storage to use, State storage is responsible for serialising / deserialising and persisting / retrieving stored stateThere are common strategies already implemented: see [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) which replicate what State (AppState, GlobalState) in legacy world did | +| [storageKey](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md) | string | Storage key to use for syncing, e.g. storageKey '\_a' should sync state to ?\_a query param | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md new file mode 100644 index 0000000000000..0098dd5c99aeb --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) > [stateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statecontainer.md) + +## IStateSyncConfig.stateContainer property + +State container to keep in sync with storage, have to implement [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) interface We encourage to use as a state container, but it is also possible to implement own custom container for advanced use cases + +Signature: + +```typescript +stateContainer: INullableBaseStateContainer; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md new file mode 100644 index 0000000000000..ef872ba0ba9b5 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) > [stateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.statestorage.md) + +## IStateSyncConfig.stateStorage property + +State storage to use, State storage is responsible for serialising / deserialising and persisting / retrieving stored state + +There are common strategies already implemented: see [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) which replicate what State (AppState, GlobalState) in legacy world did + +Signature: + +```typescript +stateStorage: StateStorage; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md new file mode 100644 index 0000000000000..d3887c23df1e0 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) > [storageKey](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.storagekey.md) + +## IStateSyncConfig.storageKey property + +Storage key to use for syncing, e.g. storageKey '\_a' should sync state to ?\_a query param + +Signature: + +```typescript +storageKey: string; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md new file mode 100644 index 0000000000000..137db68cd6b48 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) + +## ISyncStateRef interface + + +Signature: + +```typescript +export interface ISyncStateRef +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [start](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md) | StartSyncStateFnType | start state syncing | +| [stop](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md) | StopSyncStateFnType | stop state syncing | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md new file mode 100644 index 0000000000000..d8df808ba215f --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) > [start](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.start.md) + +## ISyncStateRef.start property + +start state syncing + +Signature: + +```typescript +start: StartSyncStateFnType; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md new file mode 100644 index 0000000000000..70356dd9d6c79 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) > [stop](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.stop.md) + +## ISyncStateRef.stop property + +stop state syncing + +Signature: + +```typescript +stop: StopSyncStateFnType; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md new file mode 100644 index 0000000000000..2b02c98e0d605 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.md @@ -0,0 +1,48 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) + +## kibana-plugin-plugins-kibana\_utils-public-state\_sync package + +State syncing utilities are a set of helpers for syncing your application state with URL or browser storage. + +They are designed to work together with state containers (). But state containers are not required. + +State syncing utilities include: + +- [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) util which: - Subscribes to state changes and pushes them to state storage. - Optionally subscribes to state storage changes and pushes them to state. - Two types of storage compatible with `syncState`: - [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) - Serializes state and persists it to URL's query param in rison or hashed format. Listens for state updates in the URL and pushes them back to state. - [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) - Serializes state and persists it to browser storage. + +Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples + +## Functions + +| Function | Description | +| --- | --- | +| [syncState({ storageKey, stateStorage, stateContainer, })](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) | Utility for syncing application state wrapped in state container with some kind of storage (e.g. URL) Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples | +| [syncStates(stateSyncConfigs)](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md) | | + +## Interfaces + +| Interface | Description | +| --- | --- | +| [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) | KbnUrlStateStorage is a state storage for [syncState()](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) utility which: 1. Keeps state in sync with the URL. 2. Serializes data and stores it in the URL in one of the supported formats: \* Rison encoded. \* Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See kibana's advanced option for more context state:storeInSessionStorage 3. Takes care of listening to the URL updates and notifies state about the updates. 4. Takes care of batching URL updates to prevent redundant browser history records. [GUIDE](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md) | +| [INullableBaseStateContainer](./kibana-plugin-plugins-kibana_utils-public-state_sync.inullablebasestatecontainer.md) | Extension of with one constraint: set state should handle null as incoming state | +| [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) | [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) for storing state in browser [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) | +| [IStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatestorage.md) | Any StateStorage have to implement IStateStorage interface StateStorage is responsible for: \* state serialisation / deserialization \* persisting to and retrieving from storageFor an example take a look at already implemented [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) and [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) state storages | +| [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) | Config for setting up state syncing with | +| [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) | | + +## Variables + +| Variable | Description | +| --- | --- | +| [createKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.createkbnurlstatestorage.md) | Creates [IKbnUrlStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.ikbnurlstatestorage.md) state storage | +| [createSessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.createsessionstoragestatestorage.md) | Creates [ISessionStorageStateStorage](./kibana-plugin-plugins-kibana_utils-public-state_sync.isessionstoragestatestorage.md) [guide](https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md) | + +## Type Aliases + +| Type Alias | Description | +| --- | --- | +| [StartSyncStateFnType](./kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md) | | +| [StopSyncStateFnType](./kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md) | | + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md new file mode 100644 index 0000000000000..23f71ba330d4b --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [StartSyncStateFnType](./kibana-plugin-plugins-kibana_utils-public-state_sync.startsyncstatefntype.md) + +## StartSyncStateFnType type + + +Signature: + +```typescript +export declare type StartSyncStateFnType = () => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md new file mode 100644 index 0000000000000..69ff6e899e860 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [StopSyncStateFnType](./kibana-plugin-plugins-kibana_utils-public-state_sync.stopsyncstatefntype.md) + +## StopSyncStateFnType type + + +Signature: + +```typescript +export declare type StopSyncStateFnType = () => void; +``` diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md new file mode 100644 index 0000000000000..d095c3fffc512 --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md @@ -0,0 +1,93 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [syncState](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstate.md) + +## syncState() function + +Utility for syncing application state wrapped in state container with some kind of storage (e.g. URL) Refer [here](https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync) for a complete guide and examples + +Signature: + +```typescript +export declare function syncState({ storageKey, stateStorage, stateContainer, }: IStateSyncConfig): ISyncStateRef; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { storageKey, stateStorage, stateContainer, } | IStateSyncConfig<State, IStateStorage> | | + +Returns: + +`ISyncStateRef` + +- [ISyncStateRef](./kibana-plugin-plugins-kibana_utils-public-state_sync.isyncstateref.md) + +## Remarks + +1. It is responsibility of consumer to make sure that initial app state and storage are in sync before starting syncing No initial sync happens when syncState() is called + +## Example 1 + +1. the simplest use case + +```ts +const stateStorage = createKbnUrlStateStorage(); +syncState({ + storageKey: '_s', + stateContainer, + stateStorage +}); + +``` + +## Example 2 + +2. conditionally configuring sync strategy + +```ts +const stateStorage = createKbnUrlStateStorage({useHash: config.get('state:stateContainerInSessionStorage')}) +syncState({ + storageKey: '_s', + stateContainer, + stateStorage +}); + +``` + +## Example 3 + +3. implementing custom sync strategy + +```ts +const localStorageStateStorage = { + set: (storageKey, state) => localStorage.setItem(storageKey, JSON.stringify(state)), + get: (storageKey) => localStorage.getItem(storageKey) ? JSON.parse(localStorage.getItem(storageKey)) : null +}; +syncState({ + storageKey: '_s', + stateContainer, + stateStorage: localStorageStateStorage +}); + +``` + +## Example 4 + +4. Transform state before serialising Useful for: \* Migration / backward compatibility \* Syncing part of state \* Providing default values + +```ts +const stateToStorage = (s) => ({ tab: s.tab }); +syncState({ + storageKey: '_s', + stateContainer: { + get: () => stateToStorage(stateContainer.get()), + set: stateContainer.set(({ tab }) => ({ ...stateContainer.get(), tab }), + state$: stateContainer.state$.pipe(map(stateToStorage)) + }, + stateStorage +}); + +``` + diff --git a/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md new file mode 100644 index 0000000000000..87a2449a384df --- /dev/null +++ b/docs/development/plugins/kibana_utils/public/state_sync/kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md @@ -0,0 +1,42 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-kibana\_utils-public-state\_sync](./kibana-plugin-plugins-kibana_utils-public-state_sync.md) > [syncStates](./kibana-plugin-plugins-kibana_utils-public-state_sync.syncstates.md) + +## syncStates() function + +Signature: + +```typescript +export declare function syncStates(stateSyncConfigs: Array>): ISyncStateRef; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| stateSyncConfigs | Array<IStateSyncConfig<any>> | Array of [IStateSyncConfig](./kibana-plugin-plugins-kibana_utils-public-state_sync.istatesyncconfig.md) to sync | + +Returns: + +`ISyncStateRef` + +## Example + +sync multiple different sync configs + +```ts +syncStates([ + { + storageKey: '_s1', + stateStorage: stateStorage1, + stateContainer: stateContainer1, + }, + { + storageKey: '_s2', + stateStorage: stateStorage2, + stateContainer: stateContainer2, + }, +]); + +``` + diff --git a/src/dev/run_check_published_api_changes.ts b/src/dev/run_check_published_api_changes.ts index 45dafe1b415e3..0aa450c8b002a 100644 --- a/src/dev/run_check_published_api_changes.ts +++ b/src/dev/run_check_published_api_changes.ts @@ -43,7 +43,18 @@ import getopts from 'getopts'; */ const getReportFileName = (folder: string) => { - return folder.indexOf('public') > -1 ? 'public' : 'server'; + switch (true) { + case folder.includes('public'): + return 'public'; + case folder.includes('server'): + return 'server'; + case folder.includes('common'): + return 'common'; + default: + throw new Error( + `folder "${folder}" expected to include one of ["public", "server", "common"]` + ); + } }; const apiExtractorConfig = (folder: string): ExtractorConfig => { @@ -131,7 +142,7 @@ const runApiExtractor = ( messageCallback: (message: ExtractorMessage) => { if (message.messageId === 'console-api-report-not-copied') { // ConsoleMessageId.ApiReportNotCopied - log.warning(`You have changed the signature of the ${folder} Core API`); + log.warning(`You have changed the signature of the ${folder} public API`); log.warning( 'To accept these changes run `node scripts/check_published_api_changes.js --accept` and then:\n' + "\t 1. Commit the updated documentation and API review file '" + @@ -142,7 +153,7 @@ const runApiExtractor = ( message.handled = true; } else if (message.messageId === 'console-api-report-copied') { // ConsoleMessageId.ApiReportCopied - log.warning(`You have changed the signature of the ${folder} Core API`); + log.warning(`You have changed the signature of the ${folder} public API`); log.warning( "Please commit the updated API documentation and the API review file: '" + config.reportFilePath @@ -150,7 +161,7 @@ const runApiExtractor = ( message.handled = true; } else if (message.messageId === 'console-api-report-unchanged') { // ConsoleMessageId.ApiReportUnchanged - log.info(`Core ${folder} API: no changes detected ✔`); + log.info(`${folder} API: no changes detected ✔`); message.handled = true; } }, @@ -170,7 +181,7 @@ async function run( folder: string, { log, opts }: { log: ToolingLog; opts: Options } ): Promise { - log.info(`Core ${folder} API: checking for changes in API signature...`); + log.info(`${folder} API: checking for changes in API signature...`); const { apiReportChanged, succeeded } = runApiExtractor(log, folder, opts.accept); @@ -188,7 +199,7 @@ async function run( log.error(e); return false; } - log.info(`Core ${folder} API: updated documentation ✔`); + log.info(`${folder} API: updated documentation ✔`); } // If the api signature changed or any errors or warnings occured, exit with an error @@ -224,24 +235,31 @@ async function run( opts.help = true; } - const folders = ['core/public', 'core/server', 'plugins/data/server', 'plugins/data/public']; + const core = ['core/public', 'core/server']; + const plugins = [ + 'plugins/data/server', + 'plugins/data/public', + 'plugins/kibana_utils/common/state_containers', + 'plugins/kibana_utils/public/state_sync', + ]; + const folders = [...core, ...plugins]; if (opts.help) { process.stdout.write( dedent(chalk` {dim usage:} node scripts/check_published_api_changes [...options] - Checks for any changes to the Kibana Core API + Checks for any changes to the Kibana shared API Examples: - {dim # Checks for any changes to the Kibana Core API} + {dim # Checks for any changes to the Kibana shared API} {dim $} node scripts/check_published_api_changes - {dim # Checks for any changes to the Kibana Core API and updates the documentation} + {dim # Checks for any changes to the Kibana shared API and updates the documentation} {dim $} node scripts/check_published_api_changes --docs - {dim # Checks for and automatically accepts and updates documentation for any changes to the Kibana Core API} + {dim # Checks for and automatically accepts and updates documentation for any changes to the Kibana shared API} {dim $} node scripts/check_published_api_changes --accept {dim # Only checks the core/public directory} @@ -249,7 +267,7 @@ async function run( Options: --accept {dim Accepts all changes by updating the API Review files and documentation} - --docs {dim Updates the Core API documentation} + --docs {dim Updates the API documentation} --filter {dim RegExp that folder names must match, folders: [${folders.join(', ')}]} --help {dim Show this message} `) @@ -259,20 +277,22 @@ async function run( } try { - log.info(`Core: Building types...`); + log.info(`Building types for api extractor...`); await runBuildTypes(); } catch (e) { log.error(e); return false; } - const results = await Promise.all( - folders - .filter((folder) => (opts.filter.length ? folder.match(opts.filter) : true)) - .map((folder) => run(folder, { log, opts })) + const filteredFolders = folders.filter((folder) => + opts.filter.length ? folder.match(opts.filter) : true ); + const results = []; + for (const folder of filteredFolders) { + results.push(await run(folder, { log, opts })); + } - if (results.find((r) => r === false) !== undefined) { + if (results.includes(false)) { process.exitCode = 1; } })().catch((e) => { diff --git a/src/plugins/kibana_utils/common/state_containers/common.api.md b/src/plugins/kibana_utils/common/state_containers/common.api.md new file mode 100644 index 0000000000000..f85458499b719 --- /dev/null +++ b/src/plugins/kibana_utils/common/state_containers/common.api.md @@ -0,0 +1,156 @@ +## API Report File for "kibana" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { ComponentType } from 'react'; +import { Ensure } from '@kbn/utility-types'; +import { FC } from 'react'; +import { Observable } from 'rxjs'; +import React from 'react'; + +// @public +export type BaseState = object; + +// @public +export interface BaseStateContainer { + get: () => State; + set: (state: State) => void; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "Observable" + state$: Observable; +} + +// @public +export type Comparator = (previous: Result, current: Result) => boolean; + +// @public +export type Connect = (mapStateToProp: MapStateToProps>) => (component: ComponentType) => FC>; + +// @public +export function createStateContainer(defaultState: State): ReduxLikeStateContainer; + +// @public +export function createStateContainer(defaultState: State, pureTransitions: PureTransitions): ReduxLikeStateContainer; + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "PureSelectors" +// +// @public +export function createStateContainer(defaultState: State, pureTransitions: PureTransitions, pureSelectors: PureSelectors, options?: CreateStateContainerOptions): ReduxLikeStateContainer; + +// @public +export interface CreateStateContainerOptions { + freeze?: (state: T) => T; +} + +// @public +export const createStateContainerReactHelpers: >() => { + Provider: React.Provider; + Consumer: React.Consumer; + context: React.Context; + useContainer: () => Container; + useState: () => UnboxState; + useTransitions: () => Container["transitions"]; + useSelector: (selector: (state: UnboxState) => Result, comparator?: Comparator) => Result; + connect: Connect>; +}; + +// @public +export type Dispatch = (action: T) => void; + +// @public (undocumented) +export type EnsurePureSelector = Ensure>; + +// Warning: (ae-incompatible-release-tags) The symbol "EnsurePureTransition" is marked as @public, but its signature references "PureTransition" which is marked as @internal +// +// @public (undocumented) +export type EnsurePureTransition = Ensure>; + +// @public +export type MapStateToProps = (state: State) => StateProps; + +// Warning: (ae-incompatible-release-tags) The symbol "Middleware" is marked as @public, but its signature references "TransitionDescription" which is marked as @internal +// +// @public +export type Middleware = (store: Pick, 'getState' | 'dispatch'>) => (next: (action: TransitionDescription) => TransitionDescription | any) => Dispatch; + +// @public (undocumented) +export type PureSelector = (state: State) => Selector; + +// @public (undocumented) +export type PureSelectorsToSelectors = { + [K in keyof T]: PureSelectorToSelector>; +}; + +// @public (undocumented) +export type PureSelectorToSelector> = ReturnType>; + +// @internal (undocumented) +export type PureTransition = (state: State) => Transition; + +// @internal (undocumented) +export type PureTransitionsToTransitions = { + [K in keyof T]: PureTransitionToTransition>; +}; + +// @internal (undocumented) +export type PureTransitionToTransition> = ReturnType; + +// Warning: (ae-incompatible-release-tags) The symbol "Reducer" is marked as @public, but its signature references "TransitionDescription" which is marked as @internal +// +// @public +export type Reducer = (state: State, action: TransitionDescription) => State; + +// @public +export interface ReduxLikeStateContainer extends StateContainer { + // (undocumented) + addMiddleware: (middleware: Middleware) => void; + // Warning: (ae-incompatible-release-tags) The symbol "dispatch" is marked as @public, but its signature references "TransitionDescription" which is marked as @internal + // + // (undocumented) + dispatch: (action: TransitionDescription) => void; + // (undocumented) + getState: () => State; + // (undocumented) + reducer: Reducer; + // (undocumented) + replaceReducer: (nextReducer: Reducer) => void; + // (undocumented) + subscribe: (listener: (state: State) => void) => () => void; +} + +// @public (undocumented) +export type Selector = (...args: Args) => Result; + +// @public +export interface StateContainer extends BaseStateContainer { + // (undocumented) + selectors: Readonly>; + // Warning: (ae-incompatible-release-tags) The symbol "transitions" is marked as @public, but its signature references "PureTransitionsToTransitions" which is marked as @internal + // + // (undocumented) + transitions: Readonly>; +} + +// @internal (undocumented) +export type Transition = (...args: Args) => State; + +// @internal (undocumented) +export interface TransitionDescription { + // (undocumented) + args: Args; + // (undocumented) + type: Type; +} + +// @public +export type UnboxState> = Container extends StateContainer ? T : never; + +// @public +export const useContainerSelector: , Result>(container: Container, selector: (state: UnboxState) => Result, comparator?: Comparator) => Result; + +// @public +export const useContainerState: >(container: Container) => UnboxState; + + +``` diff --git a/src/plugins/kibana_utils/common/state_containers/create_state_container.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts index 69e204a642f93..6bb6e66616c91 100644 --- a/src/plugins/kibana_utils/common/state_containers/create_state_container.ts +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts @@ -44,29 +44,57 @@ const defaultFreeze: (value: T) => T = isProduction return value as T; }; +/** + * State container options + * @public + */ export interface CreateStateContainerOptions { /** - * Function to use when freezing state. Supply identity function + * Function to use when freezing state. Supply identity function. + * If not provided, default `deepFreeze` is used. * + * @example + * If you expect that your state will be mutated externally an you cannot + * prevent that * ```ts * { * freeze: state => state, * } * ``` - * - * if you expect that your state will be mutated externally an you cannot - * prevent that. */ freeze?: (state: T) => T; } +/** + * Creates a state container without transitions and without selectors. + * @param defaultState - initial state + * @typeParam State - shape of state + * @public + */ export function createStateContainer( defaultState: State ): ReduxLikeStateContainer; +/** + * Creates a state container with transitions, but without selectors. + * @param defaultState - initial state + * @param pureTransitions - state transitions configuration object. Map of {@link PureTransition}. + * @typeParam State - shape of state + * @public + */ export function createStateContainer( defaultState: State, pureTransitions: PureTransitions ): ReduxLikeStateContainer; + +/** + * Creates a state container with transitions and selectors. + * @param defaultState - initial state + * @param pureTransitions - state transitions configuration object. Map of {@link PureTransition}. + * @param pureSelectors - state selectors configuration object. Map of {@link PureSelectors}. + * @param options - state container options {@link CreateStateContainerOptions} + * @typeParam State - shape of state + * @public + */ export function createStateContainer< State extends BaseState, PureTransitions extends object, @@ -77,14 +105,17 @@ export function createStateContainer< pureSelectors: PureSelectors, options?: CreateStateContainerOptions ): ReduxLikeStateContainer; +/** + * @internal + */ export function createStateContainer< State extends BaseState, PureTransitions extends object, PureSelectors extends object >( defaultState: State, - pureTransitions: PureTransitions = {} as PureTransitions, - pureSelectors: PureSelectors = {} as PureSelectors, + pureTransitions: PureTransitions = {} as PureTransitions, // TODO: https://github.com/elastic/kibana/issues/54439 + pureSelectors: PureSelectors = {} as PureSelectors, // TODO: https://github.com/elastic/kibana/issues/54439 options: CreateStateContainerOptions = {} ): ReduxLikeStateContainer { const { freeze = defaultFreeze } = options; diff --git a/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts index 8536f97e00ed0..4712c2fc233f8 100644 --- a/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts @@ -17,7 +17,7 @@ * under the License. */ -import * as React from 'react'; +import React from 'react'; import useObservable from 'react-use/lib/useObservable'; import defaultComparator from 'fast-deep-equal'; import { Comparator, Connect, StateContainer, UnboxState } from './types'; @@ -25,23 +25,27 @@ import { Comparator, Connect, StateContainer, UnboxState } from './types'; const { useContext, useLayoutEffect, useRef, createElement: h } = React; /** - * Returns the latest state of a state container. + * React hooks that returns the latest state of a {@link StateContainer}. * - * @param container State container which state to track. + * @param container - {@link StateContainer} which state to track. + * @returns - latest {@link StateContainer} state + * @public */ export const useContainerState = >( container: Container ): UnboxState => useObservable(container.state$, container.get()); /** - * Apply selector to state container to extract only needed information. Will + * React hook to apply selector to state container to extract only needed information. Will * re-render your component only when the section changes. * - * @param container State container which state to track. - * @param selector Function used to pick parts of state. - * @param comparator Comparator function used to memoize previous result, to not + * @param container - {@link StateContainer} which state to track. + * @param selector - Function used to pick parts of state. + * @param comparator - {@link Comparator} function used to memoize previous result, to not * re-render React component if state did not change. By default uses * `fast-deep-equal` package. + * @returns - result of a selector(state) + * @public */ export const useContainerSelector = , Result>( container: Container, @@ -68,6 +72,11 @@ export const useContainerSelector = , return value; }; +/** + * Creates helpers for using {@link StateContainer | State Containers} with react + * Refer to {@link https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_containers/react.md | guide} for details + * @public + */ export const createStateContainerReactHelpers = >() => { const context = React.createContext(null as any); diff --git a/src/plugins/kibana_utils/common/state_containers/index.ts b/src/plugins/kibana_utils/common/state_containers/index.ts index 43e204ecb79f7..e2e056bd67da2 100644 --- a/src/plugins/kibana_utils/common/state_containers/index.ts +++ b/src/plugins/kibana_utils/common/state_containers/index.ts @@ -17,6 +17,40 @@ * under the License. */ -export * from './types'; -export * from './create_state_container'; -export * from './create_state_container_react_helpers'; +/** + * State containers are Redux-store-like objects meant to help you manage state in your services or apps. + * Refer to {@link https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers | guides and examples} for more info + * + * @packageDocumentation + */ + +export { + BaseState, + BaseStateContainer, + TransitionDescription, + StateContainer, + ReduxLikeStateContainer, + Dispatch, + Middleware, + Selector, + Comparator, + MapStateToProps, + Connect, + Reducer, + UnboxState, + PureSelectorToSelector, + PureSelectorsToSelectors, + EnsurePureSelector, + PureTransitionsToTransitions, + PureTransitionToTransition, + EnsurePureTransition, + PureSelector, + PureTransition, + Transition, +} from './types'; +export { createStateContainer, CreateStateContainerOptions } from './create_state_container'; +export { + createStateContainerReactHelpers, + useContainerSelector, + useContainerState, +} from './create_state_container_react_helpers'; diff --git a/src/plugins/kibana_utils/common/state_containers/types.ts b/src/plugins/kibana_utils/common/state_containers/types.ts index 29ffa4cd486b5..b6adb89d9be7b 100644 --- a/src/plugins/kibana_utils/common/state_containers/types.ts +++ b/src/plugins/kibana_utils/common/state_containers/types.ts @@ -19,28 +19,76 @@ import { Observable } from 'rxjs'; import { Ensure } from '@kbn/utility-types'; +import { FC, ComponentType } from 'react'; +/** + * Base {@link StateContainer} state shape + * @public + */ export type BaseState = object; + +/** + * @internal + */ export interface TransitionDescription { type: Type; args: Args; } +/** + * @internal + */ export type Transition = (...args: Args) => State; +/** + * @internal + */ export type PureTransition = ( state: State ) => Transition; +/** + * @public + */ export type EnsurePureTransition = Ensure>; +/** + * @internal + */ export type PureTransitionToTransition> = ReturnType; +/** + * @internal + */ export type PureTransitionsToTransitions = { [K in keyof T]: PureTransitionToTransition>; }; +/** + * Base state container shape without transitions or selectors + * @typeParam State - Shape of state in the container. Have to match {@link BaseState} constraint + * @public + */ export interface BaseStateContainer { + /** + * Retrieves current state from the container + * @returns current state + * @public + */ get: () => State; + /** + * Sets state into container + * @param state - new state to set + */ set: (state: State) => void; + /** + * {@link Observable} of state + */ state$: Observable; } +/** + * Fully featured state container with {@link Selector | Selectors} and {@link Transition | Transitions}. Extends {@link BaseStateContainer}. + * @typeParam State - Shape of state in the container. Has to match {@link BaseState} constraint + * @typeParam PureTransitions - map of {@link PureTransition | transitions} to provide on state container + * @typeParam PureSelectors - map of {@link PureSelector | selectors} to provide on state container + * @public + */ export interface StateContainer< State extends BaseState, PureTransitions extends object = object, @@ -50,6 +98,11 @@ export interface StateContainer< selectors: Readonly>; } +/** + * Fully featured state container which matches Redux store interface. Extends {@link StateContainer}. + * Allows to use state container with redux libraries. + * @public + */ export interface ReduxLikeStateContainer< State extends BaseState, PureTransitions extends object = {}, @@ -63,45 +116,92 @@ export interface ReduxLikeStateContainer< subscribe: (listener: (state: State) => void) => () => void; } +/** + * Redux like dispatch + * @public + */ export type Dispatch = (action: T) => void; +/** + * Redux like Middleware + * @public + */ export type Middleware = ( store: Pick, 'getState' | 'dispatch'> ) => ( next: (action: TransitionDescription) => TransitionDescription | any ) => Dispatch; - +/** + * Redux like Reducer + * @public + */ export type Reducer = ( state: State, action: TransitionDescription ) => State; +/** + * Utility type for inferring state shape from {@link StateContainer} + * @public + */ export type UnboxState< Container extends StateContainer > = Container extends StateContainer ? T : never; +/** + * Utility type for inferring transitions type from {@link StateContainer} + * @public + */ export type UnboxTransitions< Container extends StateContainer > = Container extends StateContainer ? T : never; +/** + * @public + */ export type Selector = (...args: Args) => Result; +/** + * @public + */ export type PureSelector = ( state: State ) => Selector; +/** + * @public + */ export type EnsurePureSelector = Ensure>; +/** + * @public + */ export type PureSelectorToSelector> = ReturnType< EnsurePureSelector >; +/** + * @public + */ export type PureSelectorsToSelectors = { [K in keyof T]: PureSelectorToSelector>; }; +/** + * Used to compare state, see {@link useContainerSelector}. + * @public + */ export type Comparator = (previous: Result, current: Result) => boolean; - +/** + * State container state to component props mapper. + * See {@link Connect} + * @public + */ export type MapStateToProps = ( state: State ) => StateProps; +/** + * Similar to `connect` from react-redux, + * allows to map state from state container to component's props. + * @public + */ export type Connect = < Props extends object, StatePropKeys extends keyof Props >( mapStateToProp: MapStateToProps> -) => (component: React.ComponentType) => React.FC>; +) => (component: ComponentType) => FC>; diff --git a/src/plugins/kibana_utils/public/state_sync/index.ts b/src/plugins/kibana_utils/public/state_sync/index.ts index 1dfa998c5bb9d..da60b36255367 100644 --- a/src/plugins/kibana_utils/public/state_sync/index.ts +++ b/src/plugins/kibana_utils/public/state_sync/index.ts @@ -17,11 +17,32 @@ * under the License. */ +/** + * State syncing utilities are a set of helpers for syncing your application state + * with browser URL or browser storage. + * + * They are designed to work together with {@link https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_containers | state containers}. But state containers are not required. + * + * State syncing utilities include: + * + * *{@link syncState} util which: + * * Subscribes to state changes and pushes them to state storage. + * * Optionally subscribes to state storage changes and pushes them to state. + * * Two types of storages compatible with `syncState`: + * * {@link IKbnUrlStateStorage} - Serializes state and persists it to URL's query param in rison or hashed format. + * Listens for state updates in the URL and pushes them back to state. + * * {@link ISessionStorageStateStorage} - Serializes state and persists it to browser storage. + * + * Refer {@link https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync | here} for a complete guide and examples. + * @packageDocumentation + */ + export { createSessionStorageStateStorage, createKbnUrlStateStorage, IKbnUrlStateStorage, ISessionStorageStateStorage, + IStateStorage, } from './state_sync_state_storage'; export { IStateSyncConfig, INullableBaseStateContainer } from './types'; export { diff --git a/src/plugins/kibana_utils/public/state_sync/public.api.md b/src/plugins/kibana_utils/public/state_sync/public.api.md new file mode 100644 index 0000000000000..c174ba798d01a --- /dev/null +++ b/src/plugins/kibana_utils/public/state_sync/public.api.md @@ -0,0 +1,97 @@ +## API Report File for "kibana" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { History } from 'history'; +import { Observable } from 'rxjs'; + +// @public +export const createKbnUrlStateStorage: ({ useHash, history }?: { + useHash: boolean; + history?: History | undefined; +}) => IKbnUrlStateStorage; + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "Storage" +// +// @public +export const createSessionStorageStateStorage: (storage?: Storage) => ISessionStorageStateStorage; + +// @public +export interface IKbnUrlStateStorage extends IStateStorage { + cancel: () => void; + // (undocumented) + change$: (key: string) => Observable; + flush: (opts?: { + replace?: boolean; + }) => boolean; + // (undocumented) + get: (key: string) => State | null; + // (undocumented) + set: (key: string, state: State, opts?: { + replace: boolean; + }) => Promise; +} + +// Warning: (ae-forgotten-export) The symbol "BaseState" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "BaseStateContainer" needs to be exported by the entry point index.d.ts +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "BaseStateContainer" +// +// @public +export interface INullableBaseStateContainer extends BaseStateContainer { + // (undocumented) + set: (state: State | null) => void; +} + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "Storage" +// +// @public +export interface ISessionStorageStateStorage extends IStateStorage { + // (undocumented) + get: (key: string) => State | null; + // (undocumented) + set: (key: string, state: State) => void; +} + +// @public +export interface IStateStorage { + cancel?: () => void; + change$?: (key: string) => Observable; + get: (key: string) => State | null; + set: (key: string, state: State) => any; +} + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "stateSync" +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "BaseState" +// +// @public +export interface IStateSyncConfig { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "BaseStateContainer" + stateContainer: INullableBaseStateContainer; + stateStorage: StateStorage; + storageKey: string; +} + +// @public (undocumented) +export interface ISyncStateRef { + start: StartSyncStateFnType; + stop: StopSyncStateFnType; +} + +// @public (undocumented) +export type StartSyncStateFnType = () => void; + +// @public (undocumented) +export type StopSyncStateFnType = () => void; + +// @public +export function syncState({ storageKey, stateStorage, stateContainer, }: IStateSyncConfig): ISyncStateRef; + +// Warning: (ae-missing-release-tag) "syncStates" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function syncStates(stateSyncConfigs: Array>): ISyncStateRef; + + +``` diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.ts index 4c400d47b8e78..bbcaaedd0d8bf 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.ts @@ -26,29 +26,61 @@ import { distinctUntilChangedWithInitialValue } from '../../common'; import { BaseState } from '../../common/state_containers'; import { applyDiff } from '../state_management/utils/diff_object'; +/** + * @public + */ +export type StopSyncStateFnType = () => void; +/** + * @public + */ +export type StartSyncStateFnType = () => void; + +/** + * @public + */ +export interface ISyncStateRef { + /** + * stop state syncing + */ + stop: StopSyncStateFnType; + /** + * start state syncing + */ + start: StartSyncStateFnType; +} + /** * Utility for syncing application state wrapped in state container * with some kind of storage (e.g. URL) * - * Examples: + * Go {@link https://github.com/elastic/kibana/tree/master/src/plugins/kibana_utils/docs/state_sync | here} for a complete guide and examples. * - * 1. the simplest use case + * @example + * + * the simplest use case + * ```ts * const stateStorage = createKbnUrlStateStorage(); * syncState({ * storageKey: '_s', * stateContainer, * stateStorage * }); + * ``` * - * 2. conditionally configuring sync strategy + * @example + * conditionally configuring sync strategy + * ```ts * const stateStorage = createKbnUrlStateStorage({useHash: config.get('state:stateContainerInSessionStorage')}) * syncState({ * storageKey: '_s', * stateContainer, * stateStorage * }); + * ``` * - * 3. implementing custom sync strategy + * @example + * implementing custom sync strategy + * ```ts * const localStorageStateStorage = { * set: (storageKey, state) => localStorage.setItem(storageKey, JSON.stringify(state)), * get: (storageKey) => localStorage.getItem(storageKey) ? JSON.parse(localStorage.getItem(storageKey)) : null @@ -58,12 +90,15 @@ import { applyDiff } from '../state_management/utils/diff_object'; * stateContainer, * stateStorage: localStorageStateStorage * }); + * ``` * - * 4. Transform state before serialising + * @example + * transforming state before serialising * Useful for: * * Migration / backward compatibility * * Syncing part of state * * Providing default values + * ```ts * const stateToStorage = (s) => ({ tab: s.tab }); * syncState({ * storageKey: '_s', @@ -74,20 +109,12 @@ import { applyDiff } from '../state_management/utils/diff_object'; * }, * stateStorage * }); + * ``` * - * Caveats: - * - * 1. It is responsibility of consumer to make sure that initial app state and storage are in sync before starting syncing - * No initial sync happens when syncState() is called + * @param - syncing config {@link IStateSyncConfig} + * @returns - {@link ISyncStateRef} + * @public */ -export type StopSyncStateFnType = () => void; -export type StartSyncStateFnType = () => void; -export interface ISyncStateRef { - // stop syncing state with storage - stop: StopSyncStateFnType; - // start syncing state with storage - start: StartSyncStateFnType; -} export function syncState< State extends BaseState, StateStorage extends IStateStorage = IStateStorage @@ -159,7 +186,9 @@ export function syncState< } /** - * multiple different sync configs + * @example + * sync multiple different sync configs + * ```ts * syncStates([ * { * storageKey: '_s1', @@ -172,7 +201,8 @@ export function syncState< * stateContainer: stateContainer2, * }, * ]); - * @param stateSyncConfigs - Array of IStateSyncConfig to sync + * ``` + * @param stateSyncConfigs - Array of {@link IStateSyncConfig} to sync */ export function syncStates(stateSyncConfigs: Array>): ISyncStateRef { const syncRefs = stateSyncConfigs.map((config) => syncState(config)); diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts index 67c1bf26aa251..0c74e1eb9f421 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts @@ -27,6 +27,19 @@ import { setStateToKbnUrl, } from '../../state_management/url'; +/** + * KbnUrlStateStorage is a state storage for {@link syncState} utility which: + * + * 1. Keeps state in sync with the URL. + * 2. Serializes data and stores it in the URL in one of the supported formats: + * * Rison encoded. + * * Hashed URL: In URL we store only the hash from the serialized state, but the state itself is stored in sessionStorage. See Kibana's `state:storeInSessionStorage` advanced option for more context. + * 3. Takes care of listening to the URL updates and notifies state about the updates. + * 4. Takes care of batching URL updates to prevent redundant browser history records. + * + * {@link https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/kbn_url_storage.md | Refer to this guide for more info} + * @public + */ export interface IKbnUrlStateStorage extends IStateStorage { set: ( key: string, @@ -36,18 +49,23 @@ export interface IKbnUrlStateStorage extends IStateStorage { get: (key: string) => State | null; change$: (key: string) => Observable; - // cancels any pending url updates + /** + * cancels any pending url updates + */ cancel: () => void; - // synchronously runs any pending url updates - // returned boolean indicates if change occurred + /** + * Synchronously runs any pending url updates, returned boolean indicates if change occurred. + * @param opts: {replace? boolean} - allows to specify if push or replace should be used for flushing update + * @returns boolean - indicates if there was an update to flush + */ flush: (opts?: { replace?: boolean }) => boolean; } /** - * Implements syncing to/from url strategies. - * Replicates what was implemented in state (AppState, GlobalState) - * Both expanded and hashed use cases + * Creates {@link IKbnUrlStateStorage} state storage + * @returns - {@link IKbnUrlStateStorage} + * @public */ export const createKbnUrlStateStorage = ( { useHash = false, history }: { useHash: boolean; history?: History } = { useHash: false } diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts index 00edfdfd1ed61..60ff211cd590a 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_session_storage_state_storage.ts @@ -19,11 +19,23 @@ import { IStateStorage } from './types'; +/** + * {@link IStateStorage} for storing state in browser {@link Storage} + * {@link https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md | guide} + * @public + */ export interface ISessionStorageStateStorage extends IStateStorage { set: (key: string, state: State) => void; get: (key: string) => State | null; } +/** + * Creates {@link ISessionStorageStateStorage} + * {@link https://github.com/elastic/kibana/blob/master/src/plugins/kibana_utils/docs/state_sync/storages/session_storage.md | guide} + * @param storage - Option {@link Storage} to use for storing state. By default window.sessionStorage. + * @returns - {@link ISessionStorageStateStorage} + * @public + */ export const createSessionStorageStateStorage = ( storage: Storage = window.sessionStorage ): ISessionStorageStateStorage => { diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts index add1dc259be45..bae5dc2067183 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/types.ts @@ -25,7 +25,8 @@ import { Observable } from 'rxjs'; * * state serialisation / deserialization * * persisting to and retrieving from storage * - * For an example take a look at already implemented KbnUrl state storage + * For an example take a look at already implemented {@link IKbnUrlStateStorage} and {@link ISessionStorageStateStorage} state storages + * @public */ export interface IStateStorage { /** @@ -45,7 +46,7 @@ export interface IStateStorage { /** * Optional method to cancel any pending activity - * syncState() will call it, if it is provided by IStateStorage + * {@link syncState} will call it during destroy, if it is provided by IStateStorage */ cancel?: () => void; } diff --git a/src/plugins/kibana_utils/public/state_sync/types.ts b/src/plugins/kibana_utils/public/state_sync/types.ts index 2acb466d92e92..e879ab7c55b2f 100644 --- a/src/plugins/kibana_utils/public/state_sync/types.ts +++ b/src/plugins/kibana_utils/public/state_sync/types.ts @@ -20,15 +20,26 @@ import { BaseState, BaseStateContainer } from '../../common/state_containers/types'; import { IStateStorage } from './state_sync_state_storage'; +/** + * Extension of {@link BaseStateContainer} with one constraint: set state should handle `null` as incoming state + * @remarks + * State container for `stateSync()` have to accept `null` + * for example, `set()` implementation could handle null and fallback to some default state + * this is required to handle edge case, when state in storage becomes empty and syncing is in progress. + * State container will be notified about about storage becoming empty with null passed in. + * @public + */ export interface INullableBaseStateContainer extends BaseStateContainer { - // State container for stateSync() have to accept "null" - // for example, set() implementation could handle null and fallback to some default state - // this is required to handle edge case, when state in storage becomes empty and syncing is in progress. - // state container will be notified about about storage becoming empty with null passed in set: (state: State | null) => void; } +/** + * Config for setting up state syncing with {@link stateSync} + * @typeParam State - State shape to sync to storage, has to extend {@link BaseState} + * @typeParam StateStorage - used state storage to sync state with + * @public + */ export interface IStateSyncConfig< State extends BaseState, StateStorage extends IStateStorage = IStateStorage @@ -39,8 +50,8 @@ export interface IStateSyncConfig< */ storageKey: string; /** - * State container to keep in sync with storage, have to implement INullableBaseStateContainer interface - * The idea is that ./state_containers/ should be used as a state container, + * State container to keep in sync with storage, have to implement {@link INullableBaseStateContainer} interface + * We encourage to use {@link BaseStateContainer} as a state container, * but it is also possible to implement own custom container for advanced use cases */ stateContainer: INullableBaseStateContainer; @@ -49,7 +60,7 @@ export interface IStateSyncConfig< * State storage is responsible for serialising / deserialising and persisting / retrieving stored state * * There are common strategies already implemented: - * './state_sync_state_storage/' + * see {@link IKbnUrlStateStorage} * which replicate what State (AppState, GlobalState) in legacy world did * */ From 3aa59e9529f49c15904204cad26dce64eeb8d60e Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Fri, 26 Jun 2020 14:53:19 +0100 Subject: [PATCH 80/82] [alerting] migrates the old `alerting` consumer to be `alerts` (#69982) (#70052) This PR migrates all old alerts with the `alerting` consumer to have `alerts` instead. This is because in 7.9 we changed the feature ID and we need these to remain in sync otherwise the RBAC work (https://github.com/elastic/kibana/pull/67157) will break old alerts. --- .../alerts/server/saved_objects/index.ts | 2 + .../server/saved_objects/migrations.test.ts | 87 +++++ .../alerts/server/saved_objects/migrations.ts | 49 +++ .../alerting_api_integration/common/config.ts | 1 + .../tests/actions/type_not_enabled.ts | 4 +- .../spaces_only/tests/alerting/index.ts | 1 + .../spaces_only/tests/alerting/migrations.ts | 34 ++ .../{alerting => actions}/data.json | 0 .../functional/es_archives/alerts/data.json | 41 +++ .../es_archives/alerts/mappings.json | 345 ++++++++++++++++++ 10 files changed, 562 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/alerts/server/saved_objects/migrations.test.ts create mode 100644 x-pack/plugins/alerts/server/saved_objects/migrations.ts create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts rename x-pack/test/functional/es_archives/{alerting => actions}/data.json (100%) create mode 100644 x-pack/test/functional/es_archives/alerts/data.json create mode 100644 x-pack/test/functional/es_archives/alerts/mappings.json diff --git a/x-pack/plugins/alerts/server/saved_objects/index.ts b/x-pack/plugins/alerts/server/saved_objects/index.ts index c98d9bcbd9ae5..06ce8d673e6b7 100644 --- a/x-pack/plugins/alerts/server/saved_objects/index.ts +++ b/x-pack/plugins/alerts/server/saved_objects/index.ts @@ -6,6 +6,7 @@ import { SavedObjectsServiceSetup } from 'kibana/server'; import mappings from './mappings.json'; +import { getMigrations } from './migrations'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; export function setupSavedObjects( @@ -16,6 +17,7 @@ export function setupSavedObjects( name: 'alert', hidden: true, namespaceType: 'single', + migrations: getMigrations(encryptedSavedObjects), mappings: mappings.alert, }); diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts new file mode 100644 index 0000000000000..38cda5a9a0f7c --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import uuid from 'uuid'; +import { getMigrations } from './migrations'; +import { RawAlert } from '../types'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; +import { migrationMocks } from 'src/core/server/mocks'; + +const { log } = migrationMocks.createContext(); +const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); + +describe('7.9.0', () => { + beforeEach(() => { + jest.resetAllMocks(); + encryptedSavedObjectsSetup.createMigration.mockImplementation( + (shouldMigrateWhenPredicate, migration) => migration + ); + }); + + test('changes nothing on alerts by other plugins', () => { + const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.9.0']; + const alert = getMockData({}); + expect(migration790(alert, { log })).toMatchObject(alert); + + expect(encryptedSavedObjectsSetup.createMigration).toHaveBeenCalledWith( + expect.any(Function), + expect.any(Function) + ); + }); + + test('migrates the consumer for alerting', () => { + const migration790 = getMigrations(encryptedSavedObjectsSetup)['7.9.0']; + const alert = getMockData({ + consumer: 'alerting', + }); + expect(migration790(alert, { log })).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + consumer: 'alerts', + }, + }); + }); +}); + +function getMockData( + overwrites: Record = {} +): SavedObjectUnsanitizedDoc { + return { + attributes: { + enabled: true, + name: 'abc', + tags: ['foo'], + alertTypeId: '123', + consumer: 'bar', + apiKey: '', + apiKeyOwner: '', + schedule: { interval: '10s' }, + throttle: null, + params: { + bar: true, + }, + muteAll: false, + mutedInstanceIds: [], + createdBy: new Date().toISOString(), + updatedBy: new Date().toISOString(), + createdAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: '1', + actionTypeId: '1', + params: { + foo: true, + }, + }, + ], + ...overwrites, + }, + id: uuid.v4(), + type: 'alert', + }; +} diff --git a/x-pack/plugins/alerts/server/saved_objects/migrations.ts b/x-pack/plugins/alerts/server/saved_objects/migrations.ts new file mode 100644 index 0000000000000..142102dd711c7 --- /dev/null +++ b/x-pack/plugins/alerts/server/saved_objects/migrations.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + SavedObjectMigrationMap, + SavedObjectUnsanitizedDoc, + SavedObjectMigrationFn, +} from '../../../../../src/core/server'; +import { RawAlert } from '../types'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; + +export function getMigrations( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +): SavedObjectMigrationMap { + return { + '7.9.0': changeAlertingConsumer(encryptedSavedObjects), + }; +} + +/** + * In v7.9.0 we changed the Alerting plugin so it uses the `consumer` value of `alerts` + * prior to that we were using `alerting` and we need to keep these in sync + */ +function changeAlertingConsumer( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +): SavedObjectMigrationFn { + const consumerMigration = new Map(); + consumerMigration.set('alerting', 'alerts'); + + return encryptedSavedObjects.createMigration( + function shouldBeMigrated(doc): doc is SavedObjectUnsanitizedDoc { + return consumerMigration.has(doc.attributes.consumer); + }, + (doc: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc => { + const { + attributes: { consumer }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + consumer: consumerMigration.get(consumer) ?? consumer, + }, + }; + } + ); +} diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index bc209e2bb4925..0877fdc949dc4 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -82,6 +82,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) 'localhost', 'some.non.existent.com', ])}`, + '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, '--xpack.eventLog.logEntries=true', `--xpack.actions.preconfigured=${JSON.stringify({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts index 912b0dc339a21..b8963d72ead50 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/type_not_enabled.ts @@ -17,8 +17,8 @@ export default function typeNotEnabledTests({ getService }: FtrProviderContext) describe('actionType not enabled', () => { // loads action PREWRITTEN_ACTION_ID with actionType DISABLED_ACTION_TYPE - before(() => esArchiver.load('alerting')); - after(() => esArchiver.unload('alerting')); + before(() => esArchiver.load('actions')); + after(() => esArchiver.unload('actions')); it('should handle create action with disabled actionType request appropriately', async () => { const response = await supertest.post(`/api/actions/action`).set('kbn-xsrf', 'foo').send({ diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index a0c4da361bd38..2fc35ddaa3c61 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -26,5 +26,6 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./alerts_space1')); loadTestFile(require.resolve('./alerts_default_space')); loadTestFile(require.resolve('./builtin_alert_types')); + loadTestFile(require.resolve('./migrations')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts new file mode 100644 index 0000000000000..fc61f59d129d7 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { getUrlPrefix } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('alerts'); + }); + + after(async () => { + await esArchiver.unload('alerts'); + }); + + it('7.9.0 migrates the `alerting` consumer to be the `alerts`', async () => { + const response = await supertest.get( + `${getUrlPrefix(``)}/api/alerts/alert/74f3e6d7-b7bb-477d-ac28-92ee22728e6e` + ); + + expect(response.status).to.eql(200); + expect(response.body.consumer).to.equal('alerts'); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/alerting/data.json b/x-pack/test/functional/es_archives/actions/data.json similarity index 100% rename from x-pack/test/functional/es_archives/alerting/data.json rename to x-pack/test/functional/es_archives/actions/data.json diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json new file mode 100644 index 0000000000000..3703473606ea2 --- /dev/null +++ b/x-pack/test/functional/es_archives/alerts/data.json @@ -0,0 +1,41 @@ +{ + "type": "doc", + "value": { + "id": "alert:74f3e6d7-b7bb-477d-ac28-92ee22728e6e", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + ], + "alertTypeId": "example.always-firing", + "apiKey": "QIUT8u0/kbOakEHSj50jDpVR90MrqOxanEscboYOoa8PxQvcA5jfHash+fqH3b+KNjJ1LpnBcisGuPkufY9j1e32gKzwGZV5Bfys87imHvygJvIM8uKiFF8bQ8Y4NTaxOJO9fAmZPrFy07ZcQMCAQz+DUTgBFqs=", + "apiKeyOwner": "elastic", + "consumer": "alerting", + "createdAt": "2020-06-17T15:35:38.497Z", + "createdBy": "elastic", + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "always-firing-alert", + "params": { + }, + "schedule": { + "interval": "1m" + }, + "scheduledTaskId": "329798f0-b0b0-11ea-9510-fdf248d5f2a4", + "tags": [ + ], + "throttle": null, + "updatedBy": "elastic" + }, + "migrationVersion": { + "alert": "7.8.0" + }, + "references": [ + ], + "type": "alert", + "updated_at": "2020-06-17T15:35:39.839Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/alerts/mappings.json b/x-pack/test/functional/es_archives/alerts/mappings.json new file mode 100644 index 0000000000000..287d9a79a68cf --- /dev/null +++ b/x-pack/test/functional/es_archives/alerts/mappings.json @@ -0,0 +1,345 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", + "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "todo": "082a2cc96a590268344d5cd74c159ac4", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "alert": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file From 7f46f11610069566bf70e5e1a66fdf75450b918a Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 26 Jun 2020 17:01:18 +0300 Subject: [PATCH 81/82] move Metrics API to start (#69787) (#70056) * move metrics to start * update plugins accordingly * update docs * update legacy code Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../kibana-plugin-core-server.coresetup.md | 1 - ...na-plugin-core-server.coresetup.metrics.md | 13 ------ .../kibana-plugin-core-server.corestart.md | 1 + ...na-plugin-core-server.corestart.metrics.md | 12 ++++++ .../core/server/kibana-plugin-core-server.md | 2 +- ...rver.metricsservicesetup.getopsmetrics_.md | 24 ----------- ...-plugin-core-server.metricsservicesetup.md | 9 ---- src/core/server/index.ts | 6 +-- src/core/server/internal_types.ts | 4 +- src/core/server/legacy/legacy_service.test.ts | 2 - src/core/server/legacy/legacy_service.ts | 6 +-- .../server/metrics/metrics_service.mock.ts | 41 +++++++++++++------ .../server/metrics/metrics_service.test.ts | 12 +++--- src/core/server/metrics/metrics_service.ts | 13 +++--- src/core/server/metrics/types.ts | 6 +-- src/core/server/mocks.ts | 4 +- src/core/server/plugins/plugin_context.ts | 6 +-- src/core/server/server.api.md | 12 ++++-- src/core/server/server.ts | 6 +-- .../status/routes/api/register_stats.js | 2 +- .../server/__snapshots__/index.test.ts.snap | 2 +- .../server/index.test.ts | 26 +----------- .../kibana_usage_collection/server/plugin.ts | 19 +++++---- src/plugins/telemetry/server/plugin.ts | 2 +- 24 files changed, 98 insertions(+), 133 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index e9ed5b830b691..32221a320d2a1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -22,7 +22,6 @@ export interface CoreSetupStartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | | [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
} | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | | [logging](./kibana-plugin-core-server.coresetup.logging.md) | LoggingServiceSetup | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | -| [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | | [status](./kibana-plugin-core-server.coresetup.status.md) | StatusServiceSetup | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | | [uiSettings](./kibana-plugin-core-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md deleted file mode 100644 index 77c9e867ef8ea..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [metrics](./kibana-plugin-core-server.coresetup.metrics.md) - -## CoreSetup.metrics property - -[MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) - -Signature: - -```typescript -metrics: MetricsServiceSetup; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.md b/docs/development/core/server/kibana-plugin-core-server.corestart.md index 6a6bacf1eef40..acd23f0f47386 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.md @@ -19,6 +19,7 @@ export interface CoreStart | [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) | | [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | +| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | MetricsServiceStart | | | [savedObjects](./kibana-plugin-core-server.corestart.savedobjects.md) | SavedObjectsServiceStart | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) | | [uiSettings](./kibana-plugin-core-server.corestart.uisettings.md) | UiSettingsServiceStart | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md b/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md new file mode 100644 index 0000000000000..a51c2f842c346 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreStart](./kibana-plugin-core-server.corestart.md) > [metrics](./kibana-plugin-core-server.corestart.metrics.md) + +## CoreStart.metrics property + + +Signature: + +```typescript +metrics: MetricsServiceStart; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 1a03ac5ee3d1a..b7b0d8f7ed686 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -112,7 +112,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LoggerFactory](./kibana-plugin-core-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | Provides APIs to plugins for customizing the plugin's logger. | | [LogMeta](./kibana-plugin-core-server.logmeta.md) | Contextual metadata | -| [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. | +| [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | | [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) | | | [OnPostAuthToolkit](./kibana-plugin-core-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | | [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md deleted file mode 100644 index 61107fbf20ad9..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) > [getOpsMetrics$](./kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md) - -## MetricsServiceSetup.getOpsMetrics$ property - -Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) gathered. The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, based on the `opts.interval` configuration property. - -Signature: - -```typescript -getOpsMetrics$: () => Observable; -``` - -## Example - - -```ts -core.metrics.getOpsMetrics$().subscribe(metrics => { - // do something with the metrics -}) - -``` - diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md index 00045aeac74b4..0bec919797b6f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md @@ -4,17 +4,8 @@ ## MetricsServiceSetup interface -APIs to retrieves metrics gathered and exposed by the core platform. - Signature: ```typescript export interface MetricsServiceSetup ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [getOpsMetrics$](./kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md) | () => Observable<OpsMetrics> | Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) gathered. The observable will emit an initial value during core's start phase, and a new value every fixed interval of time, based on the opts.interval configuration property. | - diff --git a/src/core/server/index.ts b/src/core/server/index.ts index e0afd5e57f041..7520111bf33ac 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -60,7 +60,7 @@ import { } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; import { UuidServiceSetup } from './uuid'; -import { MetricsServiceSetup } from './metrics'; +import { MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; import { LoggingServiceSetup, @@ -403,8 +403,6 @@ export interface CoreSetup { contracts: new Map([['plugin-id', 'plugin-value']]), }, rendering: renderingServiceMock, - metrics: metricsServiceMock.createInternalSetupContract(), uuid: uuidSetup, status: statusServiceMock.createInternalSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index be737f6593c02..a544bad6c0e41 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -276,6 +276,9 @@ export class LegacyService implements CoreService { createSerializer: startDeps.core.savedObjects.createSerializer, getTypeRegistry: startDeps.core.savedObjects.getTypeRegistry, }, + metrics: { + getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$, + }, uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, }; @@ -312,9 +315,6 @@ export class LegacyService implements CoreService { logging: { configure: (config$) => setupDeps.core.logging.configure([], config$), }, - metrics: { - getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$, - }, savedObjects: { setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts index cc53a4e27d571..769f6ee2a549a 100644 --- a/src/core/server/metrics/metrics_service.mock.ts +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -16,29 +16,46 @@ * specific language governing permissions and limitations * under the License. */ - +import { BehaviorSubject } from 'rxjs'; import { MetricsService } from './metrics_service'; import { InternalMetricsServiceSetup, InternalMetricsServiceStart, - MetricsServiceSetup, MetricsServiceStart, } from './types'; -const createSetupContractMock = () => { - const setupContract: jest.Mocked = { - getOpsMetrics$: jest.fn(), - }; - return setupContract; -}; - const createInternalSetupContractMock = () => { - const setupContract: jest.Mocked = createSetupContractMock(); + const setupContract: jest.Mocked = {}; return setupContract; }; const createStartContractMock = () => { - const startContract: jest.Mocked = {}; + const startContract: jest.Mocked = { + getOpsMetrics$: jest.fn(), + }; + startContract.getOpsMetrics$.mockReturnValue( + new BehaviorSubject({ + process: { + memory: { + heap: { total_in_bytes: 1, used_in_bytes: 1, size_limit: 1 }, + resident_set_size_in_bytes: 1, + }, + event_loop_delay: 1, + pid: 1, + uptime_in_millis: 1, + }, + os: { + platform: 'darwin' as const, + platformRelease: 'test', + load: { '1m': 1, '5m': 1, '15m': 1 }, + memory: { total_in_bytes: 1, free_in_bytes: 1, used_in_bytes: 1 }, + uptime_in_millis: 1, + }, + response_times: { avg_in_millis: 1, max_in_millis: 1 }, + requests: { disconnects: 1, total: 1, statusCodes: { '200': 1 } }, + concurrent_connections: 1, + }) + ); return startContract; }; @@ -60,7 +77,7 @@ const createMock = () => { export const metricsServiceMock = { create: createMock, - createSetupContract: createSetupContractMock, + createSetupContract: createStartContractMock, createStartContract: createStartContractMock, createInternalSetupContract: createInternalSetupContractMock, createInternalStartContract: createInternalStartContractMock, diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts index b3cc06ffca1d2..f2019de7b6cab 100644 --- a/src/core/server/metrics/metrics_service.test.ts +++ b/src/core/server/metrics/metrics_service.test.ts @@ -75,8 +75,8 @@ describe('MetricsService', () => { it('resets the collector after each collection', async () => { mockOpsCollector.collect.mockResolvedValue(dummyMetrics); - const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); - await metricsService.start(); + await metricsService.setup({ http: httpMock }); + const { getOpsMetrics$ } = await metricsService.start(); // `advanceTimersByTime` only ensure the interval handler is executed // however the `reset` call is executed after the async call to `collect` @@ -109,8 +109,8 @@ describe('MetricsService', () => { describe('#stop', () => { it('stops the metrics interval', async () => { - const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); - await metricsService.start(); + await metricsService.setup({ http: httpMock }); + const { getOpsMetrics$ } = await metricsService.start(); expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); @@ -125,8 +125,8 @@ describe('MetricsService', () => { }); it('completes the metrics observable', async () => { - const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); - await metricsService.start(); + await metricsService.setup({ http: httpMock }); + const { getOpsMetrics$ } = await metricsService.start(); let completed = false; diff --git a/src/core/server/metrics/metrics_service.ts b/src/core/server/metrics/metrics_service.ts index 0ea9d00792600..f28fb21aaac0d 100644 --- a/src/core/server/metrics/metrics_service.ts +++ b/src/core/server/metrics/metrics_service.ts @@ -45,12 +45,7 @@ export class MetricsService public async setup({ http }: MetricsServiceSetupDeps): Promise { this.metricsCollector = new OpsMetricsCollector(http.server); - - const metricsObservable = this.metrics$.asObservable(); - - return { - getOpsMetrics$: () => metricsObservable, - }; + return {}; } public async start(): Promise { @@ -68,7 +63,11 @@ export class MetricsService this.refreshMetrics(); }, config.interval.asMilliseconds()); - return {}; + const metricsObservable = this.metrics$.asObservable(); + + return { + getOpsMetrics$: () => metricsObservable, + }; } private async refreshMetrics() { diff --git a/src/core/server/metrics/types.ts b/src/core/server/metrics/types.ts index 5c8f18fff380d..cbf0acacd6bab 100644 --- a/src/core/server/metrics/types.ts +++ b/src/core/server/metrics/types.ts @@ -20,12 +20,14 @@ import { Observable } from 'rxjs'; import { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics } from './collectors'; +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MetricsServiceSetup {} /** * APIs to retrieves metrics gathered and exposed by the core platform. * * @public */ -export interface MetricsServiceSetup { +export interface MetricsServiceStart { /** * Retrieve an observable emitting the {@link OpsMetrics} gathered. * The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, @@ -40,8 +42,6 @@ export interface MetricsServiceSetup { */ getOpsMetrics$: () => Observable; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface MetricsServiceStart {} export type InternalMetricsServiceSetup = MetricsServiceSetup; export type InternalMetricsServiceStart = MetricsServiceStart; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 2ac5bd98f7ed4..4491942951c50 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -129,7 +129,6 @@ function createCoreSetupMock({ http: httpMock, savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createSetupContract(), - metrics: metricsServiceMock.createSetupContract(), uiSettings: uiSettingsMock, uuid: uuidServiceMock.createSetupContract(), logging: loggingServiceMock.createSetupContract(), @@ -146,6 +145,7 @@ function createCoreStartMock() { capabilities: capabilitiesServiceMock.createStartContract(), elasticsearch: elasticsearchServiceMock.createStart(), http: httpServiceMock.createStartContract(), + metrics: metricsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), }; @@ -159,7 +159,6 @@ function createInternalCoreSetupMock() { context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createInternalSetup(), http: httpServiceMock.createInternalSetupContract(), - metrics: metricsServiceMock.createInternalSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createInternalSetupContract(), uuid: uuidServiceMock.createSetupContract(), @@ -176,6 +175,7 @@ function createInternalCoreStartMock() { capabilities: capabilitiesServiceMock.createStartContract(), elasticsearch: elasticsearchServiceMock.createStart(), http: httpServiceMock.createInternalStartContract(), + metrics: metricsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createInternalStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), }; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 32bc8dc088cad..4643789d99a88 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -169,9 +169,6 @@ export function createPluginSetupContext( logging: { configure: (config$) => deps.logging.configure(['plugins', plugin.name], config$), }, - metrics: { - getOpsMetrics$: deps.metrics.getOpsMetrics$, - }, savedObjects: { setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider, addClientWrapper: deps.savedObjects.addClientWrapper, @@ -225,6 +222,9 @@ export function createPluginStartContext( createSerializer: deps.savedObjects.createSerializer, getTypeRegistry: deps.savedObjects.getTypeRegistry, }, + metrics: { + getOpsMetrics$: deps.metrics.getOpsMetrics$, + }, uiSettings: { asScopedToClient: deps.uiSettings.asScopedToClient, }, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 4d6316fceb568..b79ce1da32251 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -712,8 +712,6 @@ export interface CoreSetup Observable; } // @public (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 3bbcd0e37e142..dc37b77c57c92 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -150,7 +150,7 @@ export class Server { savedObjects: savedObjectsSetup, }); - const metricsSetup = await this.metrics.setup({ http: httpSetup }); + await this.metrics.setup({ http: httpSetup }); const renderingSetup = await this.rendering.setup({ http: httpSetup, @@ -181,7 +181,6 @@ export class Server { status: statusSetup, uiSettings: uiSettingsSetup, uuid: uuidSetup, - metrics: metricsSetup, rendering: renderingSetup, httpResources: httpResourcesSetup, logging: loggingSetup, @@ -211,12 +210,14 @@ export class Server { }); const capabilitiesStart = this.capabilities.start(); const uiSettingsStart = await this.uiSettings.start(); + const metricsStart = await this.metrics.start(); const httpStart = this.http.getStartContract(); this.coreStart = { capabilities: capabilitiesStart, elasticsearch: elasticsearchStart, http: httpStart, + metrics: metricsStart, savedObjects: savedObjectsStart, uiSettings: uiSettingsStart, }; @@ -236,7 +237,6 @@ export class Server { await this.rendering.start({ legacy: this.legacy, }); - await this.metrics.start(); return this.coreStart; } diff --git a/src/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js index 09957e61f74d3..0221c7e0ea085 100644 --- a/src/legacy/server/status/routes/api/register_stats.js +++ b/src/legacy/server/status/routes/api/register_stats.js @@ -54,7 +54,7 @@ export function registerStatsApi(usageCollection, server, config, kbnServer) { /* kibana_stats gets singled out from the collector set as it is used * for health-checking Kibana and fetch does not rely on fetching data * from ES */ - server.newPlatform.setup.core.metrics.getOpsMetrics$().subscribe((metrics) => { + server.newPlatform.start.core.metrics.getOpsMetrics$().subscribe((metrics) => { lastMetrics = { ...metrics, timestamp: new Date().toISOString(), diff --git a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap index 41c4c33b53c8d..f07912eff02b7 100644 --- a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap +++ b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`kibana_usage_collection Runs the setup method without issues 1`] = `true`; +exports[`kibana_usage_collection Runs the setup method without issues 1`] = `false`; exports[`kibana_usage_collection Runs the setup method without issues 2`] = `true`; diff --git a/src/plugins/kibana_usage_collection/server/index.test.ts b/src/plugins/kibana_usage_collection/server/index.test.ts index c2680fef01caa..d4b065896c88c 100644 --- a/src/plugins/kibana_usage_collection/server/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/index.test.ts @@ -17,7 +17,6 @@ * under the License. */ -import { BehaviorSubject } from 'rxjs'; import { coreMock, savedObjectsRepositoryMock, @@ -47,30 +46,6 @@ describe('kibana_usage_collection', () => { test('Runs the setup method without issues', () => { const coreSetup = coreMock.createSetup(); - coreSetup.metrics.getOpsMetrics$.mockImplementation( - () => - new BehaviorSubject({ - process: { - memory: { - heap: { total_in_bytes: 1, used_in_bytes: 1, size_limit: 1 }, - resident_set_size_in_bytes: 1, - }, - event_loop_delay: 1, - pid: 1, - uptime_in_millis: 1, - }, - os: { - platform: 'darwin' as const, - platformRelease: 'test', - load: { '1m': 1, '5m': 1, '15m': 1 }, - memory: { total_in_bytes: 1, free_in_bytes: 1, used_in_bytes: 1 }, - uptime_in_millis: 1, - }, - response_times: { avg_in_millis: 1, max_in_millis: 1 }, - requests: { disconnects: 1, total: 1, statusCodes: { '200': 1 } }, - concurrent_connections: 1, - }) - ); expect(pluginInstance.setup(coreSetup, { usageCollection })).toBe(undefined); usageCollectors.forEach(({ isReady }) => { @@ -86,6 +61,7 @@ describe('kibana_usage_collection', () => { coreStart.uiSettings.asScopedToClient.mockImplementation(() => uiSettingsServiceMock.createClient() ); + expect(pluginInstance.start(coreStart)).toBe(undefined); usageCollectors.forEach(({ isReady }) => { expect(isReady()).toBe(true); // All should return true at this point diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 64d5367100236..803a9146bd08f 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -18,18 +18,18 @@ */ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { Observable } from 'rxjs'; +import { Subject, Observable } from 'rxjs'; import { PluginInitializerContext, CoreSetup, Plugin, - MetricsServiceSetup, ISavedObjectsRepository, IUiSettingsClient, SharedGlobalConfig, SavedObjectsClient, CoreStart, SavedObjectsServiceSetup, + OpsMetrics, } from '../../../core/server'; import { registerApplicationUsageCollector, @@ -49,16 +49,18 @@ export class KibanaUsageCollectionPlugin implements Plugin { private readonly legacyConfig$: Observable; private savedObjectsClient?: ISavedObjectsRepository; private uiSettingsClient?: IUiSettingsClient; + private metric$: Subject; constructor(initializerContext: PluginInitializerContext) { this.legacyConfig$ = initializerContext.config.legacy.globalConfig$; + this.metric$ = new Subject(); } public setup( - { savedObjects, metrics, getStartServices }: CoreSetup, + { savedObjects }: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup ) { - this.registerUsageCollectors(usageCollection, metrics, (opts) => + this.registerUsageCollectors(usageCollection, this.metric$, (opts) => savedObjects.registerType(opts) ); } @@ -68,19 +70,22 @@ export class KibanaUsageCollectionPlugin implements Plugin { this.savedObjectsClient = savedObjects.createInternalRepository(); const savedObjectsClient = new SavedObjectsClient(this.savedObjectsClient); this.uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + core.metrics.getOpsMetrics$().subscribe(this.metric$); } - public stop() {} + public stop() { + this.metric$.complete(); + } private registerUsageCollectors( usageCollection: UsageCollectionSetup, - metrics: MetricsServiceSetup, + metric$: Subject, registerType: SavedObjectsRegisterType ) { const getSavedObjectsClient = () => this.savedObjectsClient; const getUiSettingsClient = () => this.uiSettingsClient; - registerOpsStatsCollector(usageCollection, metrics.getOpsMetrics$()); + registerOpsStatsCollector(usageCollection, metric$); registerKibanaUsageCollector(usageCollection, this.legacyConfig$); registerManagementUsageCollector(usageCollection, getUiSettingsClient); registerUiMetricUsageCollector(usageCollection, registerType, getSavedObjectsClient); diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index e555c40d25592..6c8888feafc1f 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -75,7 +75,7 @@ export class TelemetryPlugin implements Plugin { } public async setup( - { elasticsearch, http, savedObjects, metrics }: CoreSetup, + { elasticsearch, http, savedObjects }: CoreSetup, { usageCollection, telemetryCollectionManager }: TelemetryPluginsSetup ) { const currentKibanaVersion = this.currentKibanaVersion; From 8788928218599ea649ed1739a9d4d65d6b0dc153 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 26 Jun 2020 16:20:20 +0200 Subject: [PATCH 82/82] "Explore underlying data" in-chart action (#69494) (#70061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 rename folder to "explore_data" * style: 💄 check for "share" plugin in more semantic way "explore data" actions use Discover URL generator, which is registered in "share" plugin, which is optional plugin, so we check for its existance, because otherwise URL generator is not available. * refactor: 💡 move KibanaURL to a separate file * feat: 🎸 add "Explore underlying data" in-chart action * fix: 🐛 fix imports after refactor * feat: 🎸 add start.filtersFromContext to embeddable plugin * feat: 🎸 add type checkers to data plugin * feat: 🎸 better handle empty filters in Discover URL generator * feat: 🎸 implement .getUrl() method of explore data in-chart act * feat: 🎸 add embeddable.filtersAndTimeRangeFromContext() * feat: 🎸 improve getUrl() method of explore data action * test: 💍 update test mock * fix possible stale hashHistory.location in discover * style: 💄 ensureHashHistoryLocation -> syncHistoryLocations * docs: ✏️ update autogenerated docs * test: 💍 add in-chart "Explore underlying data" unit tests * test: 💍 add in-chart "Explore underlying data" functional tests * test: 💍 clean-up custom time range after panel action tests * chore: 🤖 fix embeddable plugin mocks * chore: 🤖 fix another mock * test: 💍 add support for new action to pie chart service Co-authored-by: Anton Dosov Co-authored-by: Anton Dosov --- ...ana-plugin-plugins-data-public.isfilter.md | 11 + ...na-plugin-plugins-data-public.isfilters.md | 11 + ...bana-plugin-plugins-data-public.isquery.md | 11 + ...-plugin-plugins-data-public.istimerange.md | 11 + .../kibana-plugin-plugins-data-public.md | 4 + .../common/es_query/filters/meta_filter.ts | 10 + src/plugins/data/common/index.ts | 3 +- src/plugins/data/common/query/index.ts | 1 + src/plugins/data/common/query/is_query.ts | 27 ++ src/plugins/data/common/timefilter/index.ts | 20 ++ .../data/common/timefilter/is_time_range.ts | 26 ++ src/plugins/data/public/index.ts | 2 + src/plugins/data/public/public.api.md | 20 ++ src/plugins/discover/public/index.ts | 2 +- .../discover/public/kibana_services.ts | 15 +- src/plugins/discover/public/plugin.ts | 2 + src/plugins/discover/public/url_generator.ts | 6 +- src/plugins/embeddable/kibana.json | 1 + src/plugins/embeddable/public/index.ts | 1 + .../public/lib/triggers/triggers.ts | 14 +- src/plugins/embeddable/public/mocks.tsx | 5 + src/plugins/embeddable/public/plugin.tsx | 64 +++- .../embeddable/public/tests/test_plugin.ts | 9 +- .../services/dashboard/panel_actions.ts | 14 + .../services/visualizations/pie_chart.ts | 17 +- .../abstract_explore_data_action.ts | 74 +++++ .../explore_data_chart_action.test.ts | 274 ++++++++++++++++++ .../explore_data/explore_data_chart_action.ts | 65 +++++ .../explore_data_context_menu_action.test.ts | 28 +- .../explore_data_context_menu_action.ts | 54 ++++ .../index.ts | 1 + .../public/actions/explore_data/kibana_url.ts | 31 ++ .../public/actions/explore_data/shared.ts | 37 +++ .../discover_enhanced/public/actions/index.ts | 2 +- .../explore_data_context_menu_action.ts | 156 ---------- .../discover_enhanced/public/plugin.ts | 27 +- .../drilldowns/dashboard_drilldowns.ts | 2 +- .../drilldowns/explore_data_chart_action.ts | 98 +++++++ .../drilldowns/explore_data_panel_action.ts | 31 +- .../apps/dashboard/drilldowns/index.ts | 1 + .../services/dashboard/panel_time_range.ts | 6 + 41 files changed, 997 insertions(+), 197 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md create mode 100644 src/plugins/data/common/query/is_query.ts create mode 100644 src/plugins/data/common/timefilter/index.ts create mode 100644 src/plugins/data/common/timefilter/is_time_range.ts create mode 100644 x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts create mode 100644 x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts create mode 100644 x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts rename x-pack/plugins/discover_enhanced/public/actions/{view_in_discover => explore_data}/explore_data_context_menu_action.test.ts (88%) create mode 100644 x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts rename x-pack/plugins/discover_enhanced/public/actions/{view_in_discover => explore_data}/index.ts (86%) create mode 100644 x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts create mode 100644 x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts delete mode 100644 x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts create mode 100644 x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md new file mode 100644 index 0000000000000..f1916e89c2c98 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilter.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isFilter](./kibana-plugin-plugins-data-public.isfilter.md) + +## isFilter variable + +Signature: + +```typescript +isFilter: (x: unknown) => x is Filter +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md new file mode 100644 index 0000000000000..558da72cc26bb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isfilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isFilters](./kibana-plugin-plugins-data-public.isfilters.md) + +## isFilters variable + +Signature: + +```typescript +isFilters: (x: unknown) => x is Filter[] +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md new file mode 100644 index 0000000000000..0884566333aa8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isquery.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isQuery](./kibana-plugin-plugins-data-public.isquery.md) + +## isQuery variable + +Signature: + +```typescript +isQuery: (x: unknown) => x is Query +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md new file mode 100644 index 0000000000000..e9420493c82fb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.istimerange.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md) + +## isTimeRange variable + +Signature: + +```typescript +isTimeRange: (x: unknown) => x is TimeRange +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index f62479f02926e..feeb686a1f5ed 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -110,6 +110,10 @@ | [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {Array} | | [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | | | [injectSearchSourceReferences](./kibana-plugin-plugins-data-public.injectsearchsourcereferences.md) | | +| [isFilter](./kibana-plugin-plugins-data-public.isfilter.md) | | +| [isFilters](./kibana-plugin-plugins-data-public.isfilters.md) | | +| [isQuery](./kibana-plugin-plugins-data-public.isquery.md) | | +| [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md) | | | [parseSearchSourceJSON](./kibana-plugin-plugins-data-public.parsesearchsourcejson.md) | | | [QueryStringInput](./kibana-plugin-plugins-data-public.querystringinput.md) | | | [search](./kibana-plugin-plugins-data-public.search.md) | | 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 ff6dff9d8b749..e3099ae6a4026 100644 --- a/src/plugins/data/common/es_query/filters/meta_filter.ts +++ b/src/plugins/data/common/es_query/filters/meta_filter.ts @@ -107,3 +107,13 @@ export const pinFilter = (filter: Filter) => export const unpinFilter = (filter: Filter) => !isFilterPinned(filter) ? filter : toggleFilterPinned(filter); + +export const isFilter = (x: unknown): x is Filter => + !!x && + typeof x === 'object' && + !!(x as Filter).meta && + typeof (x as Filter).meta === 'object' && + typeof (x as Filter).meta.disabled === 'boolean'; + +export const isFilters = (x: unknown): x is Filter[] => + Array.isArray(x) && !x.find((y) => !isFilter(y)); diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index adbd93d518fc7..b40e02b709d30 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -20,11 +20,12 @@ export * from './constants'; export * from './es_query'; export * from './field_formats'; +export * from './field_mapping'; export * from './index_patterns'; export * from './kbn_field_types'; export * from './query'; export * from './search'; export * from './search/aggs'; +export * from './timefilter'; export * from './types'; export * from './utils'; -export * from './field_mapping'; diff --git a/src/plugins/data/common/query/index.ts b/src/plugins/data/common/query/index.ts index 421cc4f63e4ef..4e90f6f8bb83e 100644 --- a/src/plugins/data/common/query/index.ts +++ b/src/plugins/data/common/query/index.ts @@ -19,3 +19,4 @@ export * from './filter_manager'; export * from './types'; +export * from './is_query'; diff --git a/src/plugins/data/common/query/is_query.ts b/src/plugins/data/common/query/is_query.ts new file mode 100644 index 0000000000000..08a99a39b1ac1 --- /dev/null +++ b/src/plugins/data/common/query/is_query.ts @@ -0,0 +1,27 @@ +/* + * 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 { Query } from './types'; + +export const isQuery = (x: unknown): x is Query => + !!x && + typeof x === 'object' && + typeof (x as Query).language === 'string' && + (typeof (x as Query).query === 'string' || + (typeof (x as Query).query === 'object' && !!(x as Query).query)); diff --git a/src/plugins/data/common/timefilter/index.ts b/src/plugins/data/common/timefilter/index.ts new file mode 100644 index 0000000000000..e0c509e119fda --- /dev/null +++ b/src/plugins/data/common/timefilter/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 { isTimeRange } from './is_time_range'; diff --git a/src/plugins/data/common/timefilter/is_time_range.ts b/src/plugins/data/common/timefilter/is_time_range.ts new file mode 100644 index 0000000000000..f206cd04dde31 --- /dev/null +++ b/src/plugins/data/common/timefilter/is_time_range.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TimeRange } from './types'; + +export const isTimeRange = (x: unknown): x is TimeRange => + !!x && + typeof x === 'object' && + typeof (x as TimeRange).from === 'string' && + typeof (x as TimeRange).to === 'string'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index b0ceb6e642ee0..030d462141b09 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -439,6 +439,8 @@ export { getKbnTypeNames, } from '../common'; +export { isTimeRange, isQuery, isFilter, isFilters } from '../common'; + export * from '../common/field_mapping'; /* diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index aa9fd99e6e5ae..b1f151bb09ae6 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1301,6 +1301,26 @@ export interface ISearchStrategy { search: ISearch; } +// Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isFilter: (x: unknown) => x is Filter; + +// Warning: (ae-missing-release-tag) "isFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isFilters: (x: unknown) => x is Filter[]; + +// Warning: (ae-missing-release-tag) "isQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isQuery: (x: unknown) => x is Query; + +// Warning: (ae-missing-release-tag) "isTimeRange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isTimeRange: (x: unknown) => x is TimeRange; + // Warning: (ae-missing-release-tag) "ISyncSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 4154fdfeb3ff4..6ac8f674b6153 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -27,4 +27,4 @@ export function plugin(initializerContext: PluginInitializerContext) { export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './saved_searches'; export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; -export { DISCOVER_APP_URL_GENERATOR } from './url_generator'; +export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator'; diff --git a/src/plugins/discover/public/kibana_services.ts b/src/plugins/discover/public/kibana_services.ts index cca63cd880b60..2c6bbcc3ecce1 100644 --- a/src/plugins/discover/public/kibana_services.ts +++ b/src/plugins/discover/public/kibana_services.ts @@ -60,10 +60,23 @@ export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter createHashHistory()); +/** + * Discover currently uses two `history` instances: one from Kibana Platform and + * another from `history` package. Below function is used every time Discover + * app is loaded to synchronize both instances. + * + * This helper is temporary until https://github.com/elastic/kibana/issues/65161 is resolved. + */ +export const syncHistoryLocations = () => { + const h = getHistory(); + Object.assign(h.location, createHashHistory().location); + return h; +}; + export const [getScopedHistory, setScopedHistory] = createGetterSetter( 'scopedHistory' ); diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index ad82e0a479815..0e5c5343b6f96 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -55,6 +55,7 @@ import { setServices, setScopedHistory, getScopedHistory, + syncHistoryLocations, getServices, } from './kibana_services'; import { createSavedSearchesLoader } from './saved_searches'; @@ -245,6 +246,7 @@ export class DiscoverPlugin throw Error('Discover plugin method initializeInnerAngular is undefined'); } setScopedHistory(params.history); + syncHistoryLocations(); appMounted(); const { plugins: { data: dataStart }, diff --git a/src/plugins/discover/public/url_generator.ts b/src/plugins/discover/public/url_generator.ts index 42d689050d5ad..c7f2e2147e819 100644 --- a/src/plugins/discover/public/url_generator.ts +++ b/src/plugins/discover/public/url_generator.ts @@ -98,11 +98,13 @@ export class DiscoverUrlGenerator const queryState: QueryState = {}; if (query) appState.query = query; - if (filters) appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); + if (filters && filters.length) + appState.filters = filters?.filter((f) => !esFilters.isFilterPinned(f)); if (indexPatternId) appState.index = indexPatternId; if (timeRange) queryState.time = timeRange; - if (filters) queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); + if (filters && filters.length) + queryState.filters = filters?.filter((f) => esFilters.isFilterPinned(f)); if (refreshInterval) queryState.refreshInterval = refreshInterval; let url = `${this.params.appBasePath}#/${savedSearchPath}`; diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 535527b4d09db..74119ca158685 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -4,6 +4,7 @@ "server": false, "ui": true, "requiredPlugins": [ + "data", "inspector", "uiActions" ] diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 1d1dc79121937..9f0ccbe2b00ef 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -28,6 +28,7 @@ export { ACTION_EDIT_PANEL, Adapters, AddPanelAction, + ChartActionContext, Container, ContainerInput, ContainerOutput, diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index 2b447c89e2850..5bb96a708b7ac 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -39,10 +39,6 @@ export interface ValueClickTriggerContext { }; } -export const isValueClickTriggerContext = ( - context: ValueClickTriggerContext | RangeSelectTriggerContext -): context is ValueClickTriggerContext => context.data && 'data' in context.data; - export interface RangeSelectTriggerContext { embeddable?: T; data: { @@ -53,8 +49,16 @@ export interface RangeSelectTriggerContext }; } +export type ChartActionContext = + | ValueClickTriggerContext + | RangeSelectTriggerContext; + +export const isValueClickTriggerContext = ( + context: ChartActionContext +): context is ValueClickTriggerContext => context.data && 'data' in context.data; + export const isRangeSelectTriggerContext = ( - context: ValueClickTriggerContext | RangeSelectTriggerContext + context: ChartActionContext ): context is RangeSelectTriggerContext => context.data && 'range' in context.data; export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 49910525c7ab1..c98416cb3e8c7 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -31,6 +31,7 @@ import { coreMock } from '../../../core/public/mocks'; import { UiActionsService } from './lib/ui_actions'; import { CoreStart } from '../../../core/public'; import { Start as InspectorStart } from '../../inspector/public'; +import { dataPluginMock } from '../../data/public/mocks'; // eslint-disable-next-line import { inspectorPluginMock } from '../../inspector/public/mocks'; @@ -100,6 +101,8 @@ const createStartContract = (): Start => { EmbeddablePanel: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), + filtersAndTimeRangeFromContext: jest.fn(), + filtersFromContext: jest.fn(), }; return startContract; }; @@ -108,11 +111,13 @@ const createInstance = (setupPlugins: Partial = {}) const plugin = new EmbeddablePublicPlugin({} as any); const setup = plugin.setup(coreMock.createSetup(), { uiActions: setupPlugins.uiActions || uiActionsPluginMock.createSetupContract(), + data: dataPluginMock.createSetupContract(), }); const doStart = (startPlugins: Partial = {}) => plugin.start(coreMock.createStart(), { uiActions: startPlugins.uiActions || uiActionsPluginMock.createStartContract(), inspector: inspectorPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), }); return { plugin, diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index c4e0ca44a4e7e..03bb4a4779267 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -17,6 +17,13 @@ * under the License. */ import React from 'react'; +import { + DataPublicPluginSetup, + DataPublicPluginStart, + Filter, + TimeRange, + esFilters, +} from '../../data/public'; import { getSavedObjectFinder } from '../../saved_objects/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; import { Start as InspectorStart } from '../../inspector/public'; @@ -36,15 +43,20 @@ import { defaultEmbeddableFactoryProvider, IEmbeddable, EmbeddablePanel, + ChartActionContext, + isRangeSelectTriggerContext, + isValueClickTriggerContext, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { EmbeddableStateTransfer } from './lib/state_transfer'; export interface EmbeddableSetupDependencies { + data: DataPublicPluginSetup; uiActions: UiActionsSetup; } export interface EmbeddableStartDependencies { + data: DataPublicPluginStart; uiActions: UiActionsStart; inspector: InspectorStart; } @@ -70,6 +82,19 @@ export interface EmbeddableStart { embeddableFactoryId: string ) => EmbeddableFactory | undefined; getEmbeddableFactories: () => IterableIterator; + + /** + * Given {@link ChartActionContext} returns a list of `data` plugin {@link Filter} entries. + */ + filtersFromContext: (context: ChartActionContext) => Promise; + + /** + * Returns possible time range and filters that can be constructed from {@link ChartActionContext} object. + */ + filtersAndTimeRangeFromContext: ( + context: ChartActionContext + ) => Promise<{ filters: Filter[]; timeRange?: TimeRange }>; + EmbeddablePanel: EmbeddablePanelHOC; getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC; getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; @@ -107,7 +132,7 @@ export class EmbeddablePublicPlugin implements Plugin { this.embeddableFactories.set( @@ -121,6 +146,41 @@ export class EmbeddablePublicPlugin implements Plugin { + try { + if (isRangeSelectTriggerContext(context)) + return await data.actions.createFiltersFromRangeSelectAction(context.data); + if (isValueClickTriggerContext(context)) + return await data.actions.createFiltersFromValueClickAction(context.data); + // eslint-disable-next-line no-console + console.warn("Can't extract filters from action.", context); + } catch (error) { + // eslint-disable-next-line no-console + console.warn('Error extracting filters from action. Returning empty filter list.', error); + } + return []; + }; + + const filtersAndTimeRangeFromContext: EmbeddableStart['filtersAndTimeRangeFromContext'] = async ( + context + ) => { + const filters = await filtersFromContext(context); + + if (!context.data.timeFieldName) return { filters }; + + const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( + context.data.timeFieldName, + filters + ); + + return { + filters: restOfFilters, + timeRange: timeRangeFilter + ? esFilters.convertRangeFilterToTimeRangeString(timeRangeFilter) + : undefined, + }; + }; + const getEmbeddablePanelHoc = (stateTransfer?: EmbeddableStateTransfer) => ({ embeddable, hideHeader, @@ -146,6 +206,8 @@ export class EmbeddablePublicPlugin implements Plugin { return history ? new EmbeddableStateTransfer(core.application.navigateToApp, history) diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts index e13a906e30338..bb12e3d7b9011 100644 --- a/src/plugins/embeddable/public/tests/test_plugin.ts +++ b/src/plugins/embeddable/public/tests/test_plugin.ts @@ -23,6 +23,7 @@ import { UiActionsStart } from '../../../ui_actions/public'; import { uiActionsPluginMock } from '../../../ui_actions/public/mocks'; // eslint-disable-next-line import { inspectorPluginMock } from '../../../inspector/public/mocks'; +import { dataPluginMock } from '../../../data/public/mocks'; import { coreMock } from '../../../../core/public/mocks'; import { EmbeddablePublicPlugin, EmbeddableSetup, EmbeddableStart } from '../plugin'; @@ -42,7 +43,10 @@ export const testPlugin = ( const uiActions = uiActionsPluginMock.createPlugin(coreSetup, coreStart); const initializerContext = {} as any; const plugin = new EmbeddablePublicPlugin(initializerContext); - const setup = plugin.setup(coreSetup, { uiActions: uiActions.setup }); + const setup = plugin.setup(coreSetup, { + data: dataPluginMock.createSetupContract(), + uiActions: uiActions.setup, + }); return { plugin, @@ -51,8 +55,9 @@ export const testPlugin = ( setup, doStart: (anotherCoreStart: CoreStart = coreStart) => { const start = plugin.start(anotherCoreStart, { - uiActions: uiActionsPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), inspector: inspectorPluginMock.createStartContract(), + uiActions: uiActionsPluginMock.createStartContract(), }); return start; }, diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index c9a5dcfba32b1..0f5d6ea74a6b6 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -213,5 +213,19 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft await testSubjects.click('saveNewTitleButton'); await this.toggleContextMenu(panel); } + + async getActionWebElementByText(text: string): Promise { + log.debug(`getActionWebElement: "${text}"`); + const menu = await testSubjects.find('multipleActionsContextMenu'); + const items = await menu.findAllByCssSelector('[data-test-subj*="embeddablePanelAction-"]'); + for (const item of items) { + const currentText = await item.getVisibleText(); + if (currentText === text) { + return item; + } + } + + throw new Error(`No action matching text "${text}"`); + } })(); } diff --git a/test/functional/services/visualizations/pie_chart.ts b/test/functional/services/visualizations/pie_chart.ts index 66f32d246b31f..a25695a5bfcb7 100644 --- a/test/functional/services/visualizations/pie_chart.ts +++ b/test/functional/services/visualizations/pie_chart.ts @@ -28,10 +28,13 @@ export function PieChartProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); const defaultFindTimeout = config.get('timeouts.find'); + const panelActions = getService('dashboardPanelActions'); return new (class PieChart { - async filterOnPieSlice(name?: string) { - log.debug(`PieChart.filterOnPieSlice(${name})`); + private readonly filterActionText = 'Apply filter to current view'; + + async clickOnPieSlice(name?: string) { + log.debug(`PieChart.clickOnPieSlice(${name})`); if (name) { await testSubjects.click(`pieSlice-${name.split(' ').join('-')}`); } else { @@ -44,6 +47,16 @@ export function PieChartProvider({ getService }: FtrProviderContext) { } } + async filterOnPieSlice(name?: string) { + log.debug(`PieChart.filterOnPieSlice(${name})`); + await this.clickOnPieSlice(name); + const hasUiActionsPopup = await testSubjects.exists('multipleActionsContextMenu'); + if (hasUiActionsPopup) { + const actionElement = await panelActions.getActionWebElementByText(this.filterActionText); + await actionElement.click(); + } + } + async filterByLegendItem(label: string) { log.debug(`PieChart.filterByLegendItem(${label})`); await testSubjects.click(`legend-${label}`); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts new file mode 100644 index 0000000000000..620cabe652778 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/abstract_explore_data_action.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { DiscoverStart } from '../../../../../../src/plugins/discover/public'; +import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; +import { ViewMode, IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; +import { CoreStart } from '../../../../../../src/core/public'; +import { KibanaURL } from './kibana_url'; +import * as shared from './shared'; + +export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; + +export interface PluginDeps { + discover: Pick; + embeddable: Pick; +} + +export interface CoreDeps { + application: Pick; +} + +export interface Params { + start: StartServicesGetter; +} + +export abstract class AbstractExploreDataAction { + public readonly getIconType = (context: Context): string => 'discoverApp'; + + public readonly getDisplayName = (context: Context): string => + i18n.translate('xpack.discover.FlyoutCreateDrilldownAction.displayName', { + defaultMessage: 'Explore underlying data', + }); + + constructor(protected readonly params: Params) {} + + protected abstract async getUrl(context: Context): Promise; + + public async isCompatible({ embeddable }: Context): Promise { + if (!embeddable) return false; + if (!this.params.start().plugins.discover.urlGenerator) return false; + if (!shared.isVisualizeEmbeddable(embeddable)) return false; + if (!shared.getIndexPattern(embeddable)) return false; + if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false; + return true; + } + + public async execute(context: Context): Promise { + if (!shared.isVisualizeEmbeddable(context.embeddable)) return; + + const { core } = this.params.start(); + const { appName, appPath } = await this.getUrl(context); + + await core.application.navigateToApp(appName, { + path: appPath, + }); + } + + public async getHref(context: Context): Promise { + const { embeddable } = context; + + if (!shared.isVisualizeEmbeddable(embeddable)) { + throw new Error(`Embeddable not supported for "${this.getDisplayName(context)}" action.`); + } + + const { path } = await this.getUrl(context); + + return path; + } +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts new file mode 100644 index 0000000000000..a273f0d50e45e --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.test.ts @@ -0,0 +1,274 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExploreDataChartAction } from './explore_data_chart_action'; +import { Params, PluginDeps } from './abstract_explore_data_action'; +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; +import { + EmbeddableStart, + RangeSelectTriggerContext, + ValueClickTriggerContext, + ChartActionContext, +} from '../../../../../../src/plugins/embeddable/public'; +import { i18n } from '@kbn/i18n'; +import { + VisualizeEmbeddableContract, + VISUALIZE_EMBEDDABLE_TYPE, +} from '../../../../../../src/plugins/visualizations/public'; +import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; +import { Filter, TimeRange } from '../../../../../../src/plugins/data/public'; + +const i18nTranslateSpy = (i18n.translate as unknown) as jest.SpyInstance; + +jest.mock('@kbn/i18n', () => ({ + i18n: { + translate: jest.fn((key, options) => options.defaultMessage), + }, +})); + +afterEach(() => { + i18nTranslateSpy.mockClear(); +}); + +const setup = ({ useRangeEvent = false }: { useRangeEvent?: boolean } = {}) => { + type UrlGenerator = UrlGeneratorContract<'DISCOVER_APP_URL_GENERATOR'>; + + const core = coreMock.createStart(); + + const urlGenerator: UrlGenerator = ({ + createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), + } as unknown) as UrlGenerator; + + const filtersAndTimeRangeFromContext = jest.fn((async () => ({ + filters: [], + })) as EmbeddableStart['filtersAndTimeRangeFromContext']); + + const plugins: PluginDeps = { + discover: { + urlGenerator, + }, + embeddable: { + filtersAndTimeRangeFromContext, + }, + }; + + const params: Params = { + start: () => ({ + plugins, + self: {}, + core, + }), + }; + const action = new ExploreDataChartAction(params); + + const input = { + viewMode: ViewMode.VIEW, + }; + + const output = { + indexPatterns: [ + { + id: 'index-ptr-foo', + }, + ], + }; + + const embeddable: VisualizeEmbeddableContract = ({ + type: VISUALIZE_EMBEDDABLE_TYPE, + getInput: () => input, + getOutput: () => output, + } as unknown) as VisualizeEmbeddableContract; + + const data: ChartActionContext['data'] = { + ...(useRangeEvent + ? ({ range: {} } as RangeSelectTriggerContext['data']) + : ({ data: [] } as ValueClickTriggerContext['data'])), + timeFieldName: 'order_date', + }; + + const context = { + embeddable, + data, + } as ChartActionContext; + + return { core, plugins, urlGenerator, params, action, input, output, embeddable, data, context }; +}; + +describe('"Explore underlying data" panel action', () => { + test('action has Discover icon', () => { + const { action, context } = setup(); + expect(action.getIconType(context)).toBe('discoverApp'); + }); + + test('title is "Explore underlying data"', () => { + const { action, context } = setup(); + expect(action.getDisplayName(context)).toBe('Explore underlying data'); + }); + + test('translates title', () => { + expect(i18nTranslateSpy).toHaveBeenCalledTimes(0); + + const { action, context } = setup(); + action.getDisplayName(context); + + expect(i18nTranslateSpy).toHaveBeenCalledTimes(1); + expect(i18nTranslateSpy.mock.calls[0][0]).toBe( + 'xpack.discover.FlyoutCreateDrilldownAction.displayName' + ); + }); + + describe('isCompatible()', () => { + test('returns true when all conditions are met', async () => { + const { action, context } = setup(); + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(true); + }); + + test('returns false when URL generator is not present', async () => { + const { action, plugins, context } = setup(); + (plugins.discover as any).urlGenerator = undefined; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if embeddable is not Visualize embeddable', async () => { + const { action, embeddable, context } = setup(); + (embeddable as any).type = 'NOT_VISUALIZE_EMBEDDABLE'; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if embeddable does not have index patterns', async () => { + const { action, output, context } = setup(); + delete output.indexPatterns; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if embeddable index patterns are empty', async () => { + const { action, output, context } = setup(); + output.indexPatterns = []; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + + test('returns false if dashboard is in edit mode', async () => { + const { action, input, context } = setup(); + input.viewMode = ViewMode.EDIT; + + const isCompatible = await action.isCompatible(context); + + expect(isCompatible).toBe(false); + }); + }); + + describe('getHref()', () => { + test('returns URL path generated by URL generator', async () => { + const { action, context } = setup(); + + const href = await action.getHref(context); + + expect(href).toBe('/xyz/app/discover/foo#bar'); + }); + + test('calls URL generator with right arguments', async () => { + const { action, urlGenerator, context } = setup(); + + expect(urlGenerator.createUrl).toHaveBeenCalledTimes(0); + + await action.getHref(context); + + expect(urlGenerator.createUrl).toHaveBeenCalledTimes(1); + expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + filters: [], + indexPatternId: 'index-ptr-foo', + timeRange: undefined, + }); + }); + + test('applies chart event filters', async () => { + const { action, context, urlGenerator, plugins } = setup(); + + ((plugins.embeddable + .filtersAndTimeRangeFromContext as unknown) as jest.SpyInstance).mockImplementation(() => { + const filters: Filter[] = [ + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + ]; + const timeRange: TimeRange = { + from: 'from', + to: 'to', + }; + return { filters, timeRange }; + }); + + expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(0); + + await action.getHref(context); + + expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledTimes(1); + expect(plugins.embeddable.filtersAndTimeRangeFromContext).toHaveBeenCalledWith(context); + expect(urlGenerator.createUrl).toHaveBeenCalledWith({ + filters: [ + { + meta: { + alias: 'alias', + disabled: false, + negate: false, + }, + }, + ], + indexPatternId: 'index-ptr-foo', + timeRange: { + from: 'from', + to: 'to', + }, + }); + }); + }); + + describe('execute()', () => { + test('calls platform SPA navigation method', async () => { + const { action, context, core } = setup(); + + expect(core.application.navigateToApp).toHaveBeenCalledTimes(0); + + await action.execute(context); + + expect(core.application.navigateToApp).toHaveBeenCalledTimes(1); + }); + + test('calls platform SPA navigation method with right arguments', async () => { + const { action, context, core } = setup(); + + await action.execute(context); + + expect(core.application.navigateToApp).toHaveBeenCalledTimes(1); + expect(core.application.navigateToApp.mock.calls[0]).toEqual([ + 'discover', + { + path: '/foo#bar', + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts new file mode 100644 index 0000000000000..359f14959c6a6 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_chart_action.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from '../../../../../../src/plugins/ui_actions/public'; +import { + ValueClickTriggerContext, + RangeSelectTriggerContext, +} from '../../../../../../src/plugins/embeddable/public'; +import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; +import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public'; +import { KibanaURL } from './kibana_url'; +import * as shared from './shared'; +import { AbstractExploreDataAction } from './abstract_explore_data_action'; + +export type ExploreDataChartActionContext = ValueClickTriggerContext | RangeSelectTriggerContext; + +export const ACTION_EXPLORE_DATA_CHART = 'ACTION_EXPLORE_DATA_CHART'; + +/** + * This is "Explore underlying data" action which appears in popup context + * menu when user clicks a value in visualization or brushes a time range. + */ +export class ExploreDataChartAction extends AbstractExploreDataAction + implements Action { + public readonly id = ACTION_EXPLORE_DATA_CHART; + + public readonly type = ACTION_EXPLORE_DATA_CHART; + + public readonly order = 200; + + protected readonly getUrl = async ( + context: ExploreDataChartActionContext + ): Promise => { + const { plugins } = this.params.start(); + const { urlGenerator } = plugins.discover; + + if (!urlGenerator) { + throw new Error('Discover URL generator not available.'); + } + + const { embeddable } = context; + const { filters, timeRange } = await plugins.embeddable.filtersAndTimeRangeFromContext(context); + const state: DiscoverUrlGeneratorState = { + filters, + timeRange, + }; + + if (embeddable) { + state.indexPatternId = shared.getIndexPattern(embeddable) || undefined; + + const input = embeddable.getInput(); + + if (isTimeRange(input.timeRange) && !state.timeRange) state.timeRange = input.timeRange; + if (isQuery(input.query)) state.query = input.query; + if (isFilters(input.filters)) state.filters = [...input.filters, ...(state.filters || [])]; + } + + const path = await urlGenerator.createUrl(state); + + return new KibanaURL(path); + }; +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts similarity index 88% rename from x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts rename to x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts index a7167d2e2e691..e742b69380973 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.test.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ExploreDataContextMenuAction, - ACTION_EXPLORE_DATA, - Params, - PluginDeps, -} from './explore_data_context_menu_action'; +import { ExploreDataContextMenuAction } from './explore_data_context_menu_action'; +import { Params, PluginDeps } from './abstract_explore_data_action'; import { coreMock } from '../../../../../../src/core/public/mocks'; import { UrlGeneratorContract } from '../../../../../../src/plugins/share/public'; +import { EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; import { i18n } from '@kbn/i18n'; import { VisualizeEmbeddableContract, @@ -37,14 +34,20 @@ const setup = () => { const core = coreMock.createStart(); const urlGenerator: UrlGenerator = ({ - id: ACTION_EXPLORE_DATA, createUrl: jest.fn(() => Promise.resolve('/xyz/app/discover/foo#bar')), } as unknown) as UrlGenerator; + const filtersAndTimeRangeFromContext = jest.fn((async () => ({ + filters: [], + })) as EmbeddableStart['filtersAndTimeRangeFromContext']); + const plugins: PluginDeps = { discover: { urlGenerator, }, + embeddable: { + filtersAndTimeRangeFromContext, + }, }; const params: Params = { @@ -83,19 +86,20 @@ const setup = () => { describe('"Explore underlying data" panel action', () => { test('action has Discover icon', () => { - const { action } = setup(); - expect(action.getIconType()).toBe('discoverApp'); + const { action, context } = setup(); + expect(action.getIconType(context)).toBe('discoverApp'); }); test('title is "Explore underlying data"', () => { - const { action } = setup(); - expect(action.getDisplayName()).toBe('Explore underlying data'); + const { action, context } = setup(); + expect(action.getDisplayName(context)).toBe('Explore underlying data'); }); test('translates title', () => { expect(i18nTranslateSpy).toHaveBeenCalledTimes(0); - setup().action.getDisplayName(); + const { action, context } = setup(); + action.getDisplayName(context); expect(i18nTranslateSpy).toHaveBeenCalledTimes(1); expect(i18nTranslateSpy.mock.calls[0][0]).toBe( diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts new file mode 100644 index 0000000000000..6691089f875d8 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/explore_data_context_menu_action.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from '../../../../../../src/plugins/ui_actions/public'; +import { EmbeddableContext } from '../../../../../../src/plugins/embeddable/public'; +import { DiscoverUrlGeneratorState } from '../../../../../../src/plugins/discover/public'; +import { isTimeRange, isQuery, isFilters } from '../../../../../../src/plugins/data/public'; +import { KibanaURL } from './kibana_url'; +import * as shared from './shared'; +import { AbstractExploreDataAction } from './abstract_explore_data_action'; + +export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; + +/** + * This is "Explore underlying data" action which appears in the context + * menu of a dashboard panel. + */ +export class ExploreDataContextMenuAction extends AbstractExploreDataAction + implements Action { + public readonly id = ACTION_EXPLORE_DATA; + + public readonly type = ACTION_EXPLORE_DATA; + + public readonly order = 200; + + protected readonly getUrl = async (context: EmbeddableContext): Promise => { + const { plugins } = this.params.start(); + const { urlGenerator } = plugins.discover; + + if (!urlGenerator) { + throw new Error('Discover URL generator not available.'); + } + + const { embeddable } = context; + const state: DiscoverUrlGeneratorState = {}; + + if (embeddable) { + state.indexPatternId = shared.getIndexPattern(embeddable) || undefined; + + const input = embeddable.getInput(); + + if (isTimeRange(input.timeRange) && !state.timeRange) state.timeRange = input.timeRange; + if (isQuery(input.query)) state.query = input.query; + if (isFilters(input.filters)) state.filters = [...input.filters, ...(state.filters || [])]; + } + + const path = await urlGenerator.createUrl(state); + + return new KibanaURL(path); + }; +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts similarity index 86% rename from x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts rename to x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts index 8788621365385..e6d7d4b59149e 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/index.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/index.ts @@ -5,3 +5,4 @@ */ export * from './explore_data_context_menu_action'; +export * from './explore_data_chart_action'; diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.ts new file mode 100644 index 0000000000000..3c25fc2b3c3d1 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/kibana_url.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. + */ + +// TODO: Replace this logic with KibanaURL once it is available. +// https://github.com/elastic/kibana/issues/64497 +export class KibanaURL { + public readonly path: string; + public readonly appName: string; + public readonly appPath: string; + + constructor(path: string) { + const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/); + + if (!match) { + throw new Error('Unexpected Discover URL path.'); + } + + const [, appName, appPath] = match; + + if (!appName || !appPath) { + throw new Error('Could not parse Discover URL path.'); + } + + this.path = path; + this.appName = appName; + this.appPath = appPath; + } +} diff --git a/x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts b/x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts new file mode 100644 index 0000000000000..fa2168df944b0 --- /dev/null +++ b/x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { + VISUALIZE_EMBEDDABLE_TYPE, + VisualizeEmbeddableContract, +} from '../../../../../../src/plugins/visualizations/public'; + +export const isOutputWithIndexPatterns = ( + output: unknown +): output is { indexPatterns: Array<{ id: string }> } => { + if (!output || typeof output !== 'object') return false; + return Array.isArray((output as any).indexPatterns); +}; + +export const isVisualizeEmbeddable = ( + embeddable?: IEmbeddable +): embeddable is VisualizeEmbeddableContract => + embeddable && embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE ? true : false; + +/** + * @returns Returns empty string if no index pattern ID found. + */ +export const getIndexPattern = (embeddable?: IEmbeddable): string => { + if (!embeddable) return ''; + const output = embeddable.getOutput(); + + if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) { + return output.indexPatterns[0].id; + } + + return ''; +}; diff --git a/x-pack/plugins/discover_enhanced/public/actions/index.ts b/x-pack/plugins/discover_enhanced/public/actions/index.ts index cbb955fa46340..209ae6bee09b5 100644 --- a/x-pack/plugins/discover_enhanced/public/actions/index.ts +++ b/x-pack/plugins/discover_enhanced/public/actions/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './view_in_discover'; +export * from './explore_data'; diff --git a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts b/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts deleted file mode 100644 index d66ca129934a8..0000000000000 --- a/x-pack/plugins/discover_enhanced/public/actions/view_in_discover/explore_data_context_menu_action.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable max-classes-per-file */ - -import { i18n } from '@kbn/i18n'; -import { Action } from '../../../../../../src/plugins/ui_actions/public'; -import { DiscoverStart } from '../../../../../../src/plugins/discover/public'; -import { - EmbeddableContext, - IEmbeddable, - ViewMode, -} from '../../../../../../src/plugins/embeddable/public'; -import { StartServicesGetter } from '../../../../../../src/plugins/kibana_utils/public'; -import { CoreStart } from '../../../../../../src/core/public'; -import { - VisualizeEmbeddableContract, - VISUALIZE_EMBEDDABLE_TYPE, -} from '../../../../../../src/plugins/visualizations/public'; - -// TODO: Replace this logic with KibanaURL once it is available. -// https://github.com/elastic/kibana/issues/64497 -class KibanaURL { - public readonly path: string; - public readonly appName: string; - public readonly appPath: string; - - constructor(path: string) { - const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/); - - if (!match) { - throw new Error('Unexpected Discover URL path.'); - } - - const [, appName, appPath] = match; - - if (!appName || !appPath) { - throw new Error('Could not parse Discover URL path.'); - } - - this.path = path; - this.appName = appName; - this.appPath = appPath; - } -} - -export const ACTION_EXPLORE_DATA = 'ACTION_EXPLORE_DATA'; - -const isOutputWithIndexPatterns = ( - output: unknown -): output is { indexPatterns: Array<{ id: string }> } => { - if (!output || typeof output !== 'object') return false; - return Array.isArray((output as any).indexPatterns); -}; - -const isVisualizeEmbeddable = ( - embeddable: IEmbeddable -): embeddable is VisualizeEmbeddableContract => embeddable?.type === VISUALIZE_EMBEDDABLE_TYPE; - -export interface PluginDeps { - discover: Pick; -} - -export interface CoreDeps { - application: Pick; -} - -export interface Params { - start: StartServicesGetter; -} - -export class ExploreDataContextMenuAction implements Action { - public readonly id = ACTION_EXPLORE_DATA; - - public readonly type = ACTION_EXPLORE_DATA; - - public readonly order = 200; - - constructor(private readonly params: Params) {} - - public getDisplayName() { - return i18n.translate('xpack.discover.FlyoutCreateDrilldownAction.displayName', { - defaultMessage: 'Explore underlying data', - }); - } - - public getIconType() { - return 'discoverApp'; - } - - public async isCompatible({ embeddable }: EmbeddableContext) { - if (!this.params.start().plugins.discover.urlGenerator) return false; - if (!isVisualizeEmbeddable(embeddable)) return false; - if (!this.getIndexPattern(embeddable)) return false; - if (embeddable.getInput().viewMode !== ViewMode.VIEW) return false; - return true; - } - - public async execute({ embeddable }: EmbeddableContext) { - if (!isVisualizeEmbeddable(embeddable)) return; - - const { core } = this.params.start(); - const { appName, appPath } = await this.getUrl(embeddable); - - await core.application.navigateToApp(appName, { - path: appPath, - }); - } - - public async getHref({ embeddable }: EmbeddableContext): Promise { - if (!isVisualizeEmbeddable(embeddable)) { - throw new Error(`Embeddable not supported for "${this.getDisplayName()}" action.`); - } - - const { path } = await this.getUrl(embeddable); - - return path; - } - - private async getUrl(embeddable: VisualizeEmbeddableContract): Promise { - const { plugins } = this.params.start(); - const { urlGenerator } = plugins.discover; - - if (!urlGenerator) { - throw new Error('Discover URL generator not available.'); - } - - const { timeRange, query, filters } = embeddable.getInput(); - const indexPatternId = this.getIndexPattern(embeddable); - - const path = await urlGenerator.createUrl({ - indexPatternId, - filters, - query, - timeRange, - }); - - return new KibanaURL(path); - } - - /** - * @returns Returns empty string if no index pattern ID found. - */ - private getIndexPattern(embeddable: VisualizeEmbeddableContract): string { - const output = embeddable!.getOutput(); - - if (isOutputWithIndexPatterns(output) && output.indexPatterns.length > 0) { - return output.indexPatterns[0].id; - } - - return ''; - } -} diff --git a/x-pack/plugins/discover_enhanced/public/plugin.ts b/x-pack/plugins/discover_enhanced/public/plugin.ts index f55c5dab3449b..ea3c1222eb369 100644 --- a/x-pack/plugins/discover_enhanced/public/plugin.ts +++ b/x-pack/plugins/discover_enhanced/public/plugin.ts @@ -6,7 +6,12 @@ import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { PluginInitializerContext } from 'kibana/public'; -import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { + UiActionsSetup, + UiActionsStart, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../src/plugins/ui_actions/public'; import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public'; import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; @@ -16,11 +21,18 @@ import { EmbeddableContext, CONTEXT_MENU_TRIGGER, } from '../../../../src/plugins/embeddable/public'; -import { ExploreDataContextMenuAction, ACTION_EXPLORE_DATA } from './actions'; +import { + ExploreDataContextMenuAction, + ExploreDataChartAction, + ACTION_EXPLORE_DATA, + ACTION_EXPLORE_DATA_CHART, + ExploreDataChartActionContext, +} from './actions'; declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { [ACTION_EXPLORE_DATA]: EmbeddableContext; + [ACTION_EXPLORE_DATA_CHART]: ExploreDataChartActionContext; } } @@ -48,10 +60,17 @@ export class DiscoverEnhancedPlugin { uiActions, share }: DiscoverEnhancedSetupDependencies ) { const start = createStartServicesGetter(core.getStartServices); + const isSharePluginInstalled = !!share; - if (!!share) { - const exploreDataAction = new ExploreDataContextMenuAction({ start }); + if (isSharePluginInstalled) { + const params = { start }; + + const exploreDataAction = new ExploreDataContextMenuAction(params); uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, exploreDataAction); + + const exploreDataChartAction = new ExploreDataChartAction(params); + uiActions.addTriggerAction(SELECT_RANGE_TRIGGER, exploreDataChartAction); + uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, exploreDataChartAction); } } diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts index bcdd3d1f82e7d..29ead0db1c634 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_drilldowns.ts @@ -59,7 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); // trigger drilldown action by clicking on a pie and picking drilldown action by it's name - await pieChart.filterOnPieSlice('40,000'); + await pieChart.clickOnPieSlice('40,000'); await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); const href = await dashboardDrilldownPanelActions.getActionHrefByText( diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts new file mode 100644 index 0000000000000..12363f8800c28 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_chart_action.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const ACTION_ID = 'ACTION_EXPLORE_DATA_CHART'; +const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const drilldowns = getService('dashboardDrilldownsManage'); + const { dashboard, discover, common, timePicker } = getPageObjects([ + 'dashboard', + 'discover', + 'common', + 'timePicker', + ]); + const testSubjects = getService('testSubjects'); + const pieChart = getService('pieChart'); + const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); + const filterBar = getService('filterBar'); + const browser = getService('browser'); + + describe('Explore underlying data - chart action', () => { + describe('value click action', () => { + it('action exists in chart click popup menu', async () => { + await common.navigateToApp('dashboard'); + await dashboard.preserveCrossAppState(); + await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); + await pieChart.clickOnPieSlice('160,000'); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); + }); + + it('action is a link element', async () => { + const actionElement = await testSubjects.find(ACTION_TEST_SUBJ); + const tag = await actionElement.getTagName(); + const href = await actionElement.getAttribute('href'); + + expect(tag.toLowerCase()).to.be('a'); + expect(typeof href).to.be('string'); + expect(href.length > 5).to.be(true); + }); + + it('navigates to Discover app on action click carrying over pie slice filter', async () => { + await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); + await discover.waitForDiscoverAppOnScreen(); + await filterBar.hasFilter('memory', '160,000 to 200,000'); + const filterCount = await filterBar.getFilterCount(); + + expect(filterCount).to.be(1); + }); + }); + + describe('brush action', () => { + let originalTimeRangeDurationHours: number | undefined; + + it('action exists in chart brush popup menu', async () => { + await common.navigateToApp('dashboard'); + await dashboard.preserveCrossAppState(); + await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_AREA_CHART_NAME); + + originalTimeRangeDurationHours = await timePicker.getTimeDurationInHours(); + const areaChart = await testSubjects.find('visualizationLoader'); + await browser.dragAndDrop( + { + location: areaChart, + offset: { + x: -100, + y: 0, + }, + }, + { + location: areaChart, + offset: { + x: 100, + y: 0, + }, + } + ); + + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); + }); + + it('navigates to Discover on click carrying over brushed time range', async () => { + await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); + await discover.waitForDiscoverAppOnScreen(); + const newTimeRangeDurationHours = await timePicker.getTimeDurationInHours(); + + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours as number); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts index 24d6e820ac0eb..fedc83a2f81c7 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/explore_data_panel_action.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; const ACTION_ID = 'ACTION_EXPLORE_DATA'; -const EXPLORE_RAW_DATA_ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; +const ACTION_TEST_SUBJ = `embeddablePanelAction-${ACTION_ID}`; export default function ({ getService, getPageObjects }: FtrProviderContext) { const drilldowns = getService('dashboardDrilldownsManage'); @@ -24,31 +24,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); describe('Explore underlying data - panel action', function () { - before(async () => { - await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash*' }); + before( + 'change default index pattern to verify action navigates to correct index pattern', + async () => { + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash*' }); + } + ); + + before('start on Dashboard landing page', async () => { await common.navigateToApp('dashboard'); await dashboard.preserveCrossAppState(); }); - after(async () => { + after('set back default index pattern', async () => { await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); }); + after('clean-up custom time range on panel', async () => { + await common.navigateToApp('dashboard'); + await dashboard.gotoDashboardEditMode(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); + await panelActions.openContextMenu(); + await panelActionsTimeRange.clickTimeRangeActionInContextMenu(); + await panelActionsTimeRange.clickRemovePerPanelTimeRangeButton(); + await dashboard.saveDashboard('Dashboard with Pie Chart'); + }); + it('action exists in panel context menu', async () => { await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); await panelActions.openContextMenu(); - await testSubjects.existOrFail(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + await testSubjects.existOrFail(ACTION_TEST_SUBJ); }); it('is a link element', async () => { - const actionElement = await testSubjects.find(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + const actionElement = await testSubjects.find(ACTION_TEST_SUBJ); const tag = await actionElement.getTagName(); expect(tag.toLowerCase()).to.be('a'); }); it('navigates to Discover app to index pattern of the panel on action click', async () => { - await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); await discover.waitForDiscoverAppOnScreen(); const el = await testSubjects.find('indexPattern-switch-link'); @@ -71,7 +86,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboard.saveDashboard('Dashboard with Pie Chart'); await panelActions.openContextMenu(); - await testSubjects.clickWhenNotDisabled(EXPLORE_RAW_DATA_ACTION_TEST_SUBJ); + await testSubjects.clickWhenNotDisabled(ACTION_TEST_SUBJ); await discover.waitForDiscoverAppOnScreen(); const text = await timePicker.getShowDatesButtonText(); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts index 19d85ad0e448f..4cdb33c06947f 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/index.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/index.ts @@ -24,5 +24,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_drilldowns')); loadTestFile(require.resolve('./explore_data_panel_action')); + loadTestFile(require.resolve('./explore_data_chart_action')); }); } diff --git a/x-pack/test/functional/services/dashboard/panel_time_range.ts b/x-pack/test/functional/services/dashboard/panel_time_range.ts index 6a91a6ff0584b..f71e8284c30d9 100644 --- a/x-pack/test/functional/services/dashboard/panel_time_range.ts +++ b/x-pack/test/functional/services/dashboard/panel_time_range.ts @@ -52,5 +52,11 @@ export function DashboardPanelTimeRangeProvider({ getService }: FtrProviderConte const button = await this.findModalTestSubject('addPerPanelTimeRangeButton'); await button.click(); } + + public async clickRemovePerPanelTimeRangeButton() { + log.debug('clickRemovePerPanelTimeRangeButton'); + const button = await this.findModalTestSubject('removePerPanelTimeRangeButton'); + await button.click(); + } })(); }